From b29822efc7821ecba342fa09e9816dd65e853d3e Mon Sep 17 00:00:00 2001 From: latte Date: Tue, 13 Jan 2026 17:20:52 +0000 Subject: [PATCH] added technical documentation --- docs/README.md | 195 ++++++++ docs/architecture.md | 431 +++++++++++++++++ docs/configuration.md | 637 ++++++++++++++++++++++++++ docs/database.md | 570 +++++++++++++++++++++++ docs/guides/README.md | 589 ++++++++++++++++++++++++ docs/living-ai/README.md | 301 ++++++++++++ docs/living-ai/fact-extraction.md | 441 ++++++++++++++++++ docs/living-ai/mood-system.md | 338 ++++++++++++++ docs/living-ai/opinion-system.md | 418 +++++++++++++++++ docs/living-ai/relationship-system.md | 417 +++++++++++++++++ docs/services/README.md | 615 +++++++++++++++++++++++++ schema.sql | 2 +- 12 files changed, 4953 insertions(+), 1 deletion(-) create mode 100644 docs/README.md create mode 100644 docs/architecture.md create mode 100644 docs/configuration.md create mode 100644 docs/database.md create mode 100644 docs/guides/README.md create mode 100644 docs/living-ai/README.md create mode 100644 docs/living-ai/fact-extraction.md create mode 100644 docs/living-ai/mood-system.md create mode 100644 docs/living-ai/opinion-system.md create mode 100644 docs/living-ai/relationship-system.md create mode 100644 docs/services/README.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..7d9fa6e --- /dev/null +++ b/docs/README.md @@ -0,0 +1,195 @@ +# Daemon Boyfriend - Technical Documentation + +Welcome to the technical documentation for Daemon Boyfriend, a Discord bot with AI-powered personality, emotional depth, and relationship awareness. + +## Quick Navigation + +| Document | Description | +|----------|-------------| +| [Architecture Overview](architecture.md) | High-level system design, components, data flow | +| [Living AI System](living-ai/README.md) | Mood, relationships, fact extraction, opinions | +| [Services Reference](services/README.md) | API documentation for all services | +| [Database Schema](database.md) | Complete database schema reference | +| [Configuration](configuration.md) | All environment variables and settings | +| [Developer Guides](guides/README.md) | How to extend and develop the bot | + +--- + +## What is Daemon Boyfriend? + +Daemon Boyfriend is a Discord bot that goes beyond simple chatbot functionality. It features a sophisticated "Living AI" system that gives the bot: + +- **Emotional States** - A mood system based on psychological valence-arousal model +- **Relationship Tracking** - Evolving relationships from stranger to close friend +- **Memory** - Learns and remembers facts about users +- **Opinions** - Develops opinions on topics through discussion +- **Personality Adaptation** - Learns each user's communication style + +--- + +## Architecture at a Glance + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Discord API │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Discord Bot │ +│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ +│ │ AIChatCog │ │ MemoryCog │ │ StatusCog │ │ +│ └────────────┘ └────────────┘ └────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + │ + ┌───────────────────┼───────────────────┐ + ▼ ▼ ▼ +┌────────────────┐ ┌────────────────┐ ┌────────────────────────┐ +│ Core Services │ │ Living AI │ │ AI Providers │ +│ │ │ │ │ │ +│ UserService │ │ MoodService │ │ OpenAI / Anthropic / │ +│ DatabaseService│ │ RelationshipSvc│ │ Gemini / OpenRouter │ +│ ConversationMgr│ │ FactExtraction │ │ │ +│ SearXNGService │ │ OpinionService │ │ │ +└────────────────┘ └────────────────┘ └────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ PostgreSQL Database │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Documentation Structure + +### Core Documentation + +| File | Contents | +|------|----------| +| [architecture.md](architecture.md) | System architecture, design patterns, component interactions | +| [database.md](database.md) | Database schema, tables, indexes, relationships | +| [configuration.md](configuration.md) | All configuration options with examples | + +### Living AI Deep Dives + +| File | Contents | +|------|----------| +| [living-ai/README.md](living-ai/README.md) | Overview of all Living AI systems | +| [living-ai/mood-system.md](living-ai/mood-system.md) | Valence-arousal model, mood labels, decay | +| [living-ai/relationship-system.md](living-ai/relationship-system.md) | Relationship levels, scoring algorithm | +| [living-ai/fact-extraction.md](living-ai/fact-extraction.md) | AI-powered fact learning, deduplication | +| [living-ai/opinion-system.md](living-ai/opinion-system.md) | Topic sentiment, interest tracking | + +### Developer Resources + +| File | Contents | +|------|----------| +| [services/README.md](services/README.md) | Complete API reference for all services | +| [guides/README.md](guides/README.md) | How to add providers, commands, features | + +--- + +## Key Concepts + +### Message Flow + +1. User @mentions the bot in Discord +2. `AIChatCog` receives the message +3. User context is built (name, facts, preferences) +4. Living AI context is gathered (mood, relationship, style, opinions) +5. Enhanced system prompt is constructed +6. AI provider generates response +7. Living AI systems are updated (mood, relationship, facts) +8. Response is sent to Discord + +### Living AI Components + +| Component | Purpose | Update Frequency | +|-----------|---------|------------------| +| **Mood** | Emotional state | Every interaction | +| **Relationship** | User familiarity | Every interaction | +| **Facts** | User knowledge | ~30% of messages | +| **Opinions** | Topic preferences | When topics discussed | +| **Style** | Communication preferences | Rolling 50 messages | + +### Database Modes + +| Mode | Configuration | Use Case | +|------|---------------|----------| +| **PostgreSQL** | `DATABASE_URL` set | Production, persistence | +| **In-Memory** | `DATABASE_URL` not set | Development, testing | + +--- + +## Quick Start + +### 1. Install + +```bash +pip install -r requirements.txt +pip install -e . +``` + +### 2. Configure + +```bash +# Minimum .env +DISCORD_TOKEN=your_token +OPENAI_API_KEY=your_key +``` + +### 3. Run + +```bash +python -m daemon_boyfriend +``` + +### 4. Interact + +Mention the bot in Discord: +``` +@Daemon Hello! How are you today? +``` + +--- + +## Commands Reference + +### User Commands + +| Command | Description | +|---------|-------------| +| `!setname ` | Set your preferred name | +| `!clearname` | Reset to Discord name | +| `!remember ` | Tell the bot something about you | +| `!whatdoyouknow` | See what the bot remembers | +| `!forgetme` | Clear all your facts | +| `!relationship` | See your relationship status | +| `!mood` | See the bot's current mood | +| `!botstats` | View bot statistics | +| `!ourhistory` | See your history with the bot | +| `!birthday ` | Set your birthday | + +### Admin Commands + +| Command | Description | +|---------|-------------| +| `!setusername @user ` | Set name for another user | +| `!teachbot @user ` | Add a fact about a user | +| `!status` | View bot health metrics | + +--- + +## Support + +- **Project Issues:** GitHub Issues +- **Documentation Updates:** Submit a PR + +--- + +## Document Changelog + +| Version | Date | Changes | +|---------|------|---------| +| 1.0 | 2024-01 | Initial documentation | diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..551bcb7 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,431 @@ +# Architecture Overview + +This document provides a comprehensive overview of the Daemon Boyfriend Discord bot architecture. + +## Table of Contents + +- [High-Level Architecture](#high-level-architecture) +- [Directory Structure](#directory-structure) +- [Core Components](#core-components) +- [Data Flow](#data-flow) +- [Design Patterns](#design-patterns) +- [Component Interaction Diagram](#component-interaction-diagram) + +--- + +## High-Level Architecture + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Discord API │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Discord Bot (bot.py) │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ AIChatCog │ │ MemoryCog │ │ StatusCog │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ + ┌───────────────┼───────────────┐ + ▼ ▼ ▼ +┌───────────────────────┐ ┌─────────────────┐ ┌─────────────────────────────┐ +│ Core Services │ │ Living AI │ │ AI Providers │ +│ ┌─────────────────┐ │ │ Services │ │ ┌─────────────────────┐ │ +│ │ UserService │ │ │ ┌───────────┐ │ │ │ OpenAI Provider │ │ +│ │ DatabaseService│ │ │ │ MoodSvc │ │ │ │ OpenRouter Provider│ │ +│ │ ConversationMgr│ │ │ │ RelSvc │ │ │ │ Anthropic Provider │ │ +│ │ SearXNGService │ │ │ │ FactSvc │ │ │ │ Gemini Provider │ │ +│ └─────────────────┘ │ │ │ OpinionSvc│ │ │ └─────────────────────┘ │ +└───────────────────────┘ │ │ StyleSvc │ │ └─────────────────────────────┘ + │ │ ProactSvc │ │ + │ │ AssocSvc │ │ + │ │ SelfAware │ │ + │ └───────────┘ │ + └─────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ PostgreSQL Database │ +│ ┌──────────────────────────────┐ ┌──────────────────────────────────────┐ │ +│ │ Core Tables │ │ Living AI Tables │ │ +│ │ users, guilds, conversations │ │ bot_states, user_relationships, │ │ +│ │ messages, user_facts │ │ mood_history, scheduled_events │ │ +│ └──────────────────────────────┘ └──────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Directory Structure + +``` +daemon-boyfriend/ +├── src/daemon_boyfriend/ +│ ├── __main__.py # Entry point +│ ├── bot.py # Discord bot setup & cog loading +│ ├── config.py # Pydantic settings configuration +│ │ +│ ├── cogs/ # Discord command handlers +│ │ ├── ai_chat.py # @mention response handler +│ │ ├── memory.py # Memory management commands +│ │ └── status.py # Bot status & health commands +│ │ +│ ├── models/ # SQLAlchemy ORM models +│ │ ├── base.py # Base model with PortableJSON +│ │ ├── user.py # User, UserFact, UserPreference +│ │ ├── conversation.py # Conversation, Message +│ │ ├── guild.py # Guild, GuildMember +│ │ └── living_ai.py # All Living AI models +│ │ +│ └── services/ # Business logic layer +│ ├── ai_service.py # AI provider factory +│ ├── database.py # Database connection management +│ ├── user_service.py # User CRUD operations +│ ├── conversation.py # In-memory conversation manager +│ ├── persistent_conversation.py # DB-backed conversations +│ ├── searxng.py # Web search integration +│ ├── monitoring.py # Health & metrics tracking +│ │ +│ ├── providers/ # AI provider implementations +│ │ ├── base.py # Abstract AIProvider +│ │ ├── openai.py +│ │ ├── openrouter.py +│ │ ├── anthropic.py +│ │ └── gemini.py +│ │ +│ └── [Living AI Services] +│ ├── mood_service.py +│ ├── relationship_service.py +│ ├── fact_extraction_service.py +│ ├── opinion_service.py +│ ├── communication_style_service.py +│ ├── proactive_service.py +│ ├── association_service.py +│ └── self_awareness_service.py +│ +├── alembic/ # Database migrations +├── tests/ # Test suite +├── schema.sql # Database schema definition +├── docker-compose.yml # Docker deployment +└── requirements.txt # Python dependencies +``` + +--- + +## Core Components + +### 1. Discord Bot Layer + +The bot uses [discord.py](https://discordpy.readthedocs.io/) with a cog-based architecture. + +**Entry Point (`__main__.py`)** +- Creates the bot instance +- Initializes database connection +- Starts the Discord event loop + +**Bot Setup (`bot.py`)** +- Configures Discord intents +- Auto-loads all cogs from `cogs/` directory +- Handles bot lifecycle events + +**Cogs** + +| Cog | Purpose | Trigger | +|-----|---------|---------| +| `AIChatCog` | Main conversation handler | `@mention` | +| `MemoryCog` | User memory management | `!setname`, `!remember`, etc. | +| `StatusCog` | Health & metrics | `!status` (admin) | + +### 2. Service Layer + +Services contain all business logic and are designed to be: +- **Stateless**: No instance state, all state in database +- **Async-first**: All I/O operations are async +- **Testable**: Accept database sessions as parameters + +**Core Services** + +| Service | Responsibility | +|---------|----------------| +| `AIService` | Factory for AI providers, prompt enhancement | +| `DatabaseService` | Connection pool, session management | +| `UserService` | User CRUD, facts, preferences | +| `ConversationManager` | In-memory conversation history | +| `PersistentConversationManager` | Database-backed conversations | +| `SearXNGService` | Web search for current information | +| `MonitoringService` | Health checks, metrics, error tracking | + +**Living AI Services** (see [Living AI documentation](living-ai/README.md)) + +| Service | Responsibility | +|---------|----------------| +| `MoodService` | Emotional state management | +| `RelationshipService` | User relationship tracking | +| `FactExtractionService` | Autonomous fact learning | +| `OpinionService` | Topic sentiment tracking | +| `CommunicationStyleService` | User style preferences | +| `ProactiveService` | Scheduled events & follow-ups | +| `AssociationService` | Cross-user memory linking | +| `SelfAwarenessService` | Bot statistics & reflection | + +### 3. AI Provider Layer + +Uses the **Provider Pattern** to support multiple AI backends. + +``` +┌────────────────────────────────────────────────────────────┐ +│ AIService │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ get_provider() → returns appropriate AIProvider │ │ +│ │ chat() → builds context and calls provider.generate │ │ +│ └─────────────────────────────────────────────────────┘ │ +└────────────────────────────────────────────────────────────┘ + │ + ┌─────────────────┼─────────────────┐ + ▼ ▼ ▼ +┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ +│ OpenAI Provider │ │ Anthropic Provider│ │ Gemini Provider │ +│ (+ OpenRouter) │ │ │ │ │ +└──────────────────┘ └──────────────────┘ └──────────────────┘ +``` + +### 4. Data Layer + +**Models** (`models/`) +- SQLAlchemy ORM with async support +- `PortableJSON` type for PostgreSQL/SQLite compatibility +- Timezone-aware UTC timestamps + +**Database** (`services/database.py`) +- Async PostgreSQL via `asyncpg` +- Connection pooling +- Graceful fallback to in-memory when no database + +--- + +## Data Flow + +### Message Processing Flow + +``` +1. User sends @mention in Discord + │ + ▼ +2. AIChatCog.on_message() receives event + │ + ▼ +3. Get or create User via UserService + │ + ▼ +4. Get or create Conversation + (PersistentConversationManager or ConversationManager) + │ + ▼ +5. Build conversation history from messages + │ + ▼ +6. [Optional] Check if web search needed + └─► SearXNGService.search() → add results to context + │ + ▼ +7. Apply Living AI enhancements: + ├─► MoodService.get_current_mood() + ├─► RelationshipService.get_relationship() + ├─► CommunicationStyleService.get_style() + └─► OpinionService.get_relevant_opinions() + │ + ▼ +8. Build enhanced system prompt with all context + │ + ▼ +9. AIService.chat() → Provider.generate() + │ + ▼ +10. Post-response processing: + ├─► MoodService.update_mood() + ├─► RelationshipService.record_interaction() + ├─► CommunicationStyleService.record_message() + ├─► FactExtractionService.maybe_extract_facts() + └─► ProactiveService.check_for_events() + │ + ▼ +11. Split response if > 2000 chars + │ + ▼ +12. Send response(s) to Discord +``` + +### Database Fallback Flow + +``` +┌─────────────────────────────────────────────────┐ +│ Application Start │ +└─────────────────────────────────────────────────┘ + │ + ▼ + ┌────────────────────────┐ + │ DATABASE_URL set? │ + └────────────────────────┘ + │ │ + Yes No + │ │ + ▼ ▼ +┌───────────────────┐ ┌───────────────────────┐ +│ PostgreSQL Mode │ │ In-Memory Mode │ +│ - Full persistence│ │ - ConversationManager │ +│ - Living AI state │ │ - No persistence │ +│ - User facts │ │ - Basic functionality │ +└───────────────────┘ └───────────────────────┘ +``` + +--- + +## Design Patterns + +### 1. Provider Pattern (AI Services) + +Abstract base class with multiple implementations. + +```python +# base.py +class AIProvider(ABC): + @abstractmethod + async def generate( + self, + messages: list[Message], + system_prompt: str | None = None, + max_tokens: int = 1024, + temperature: float = 0.7, + ) -> AIResponse: + pass + +# openai.py +class OpenAIProvider(AIProvider): + async def generate(...) -> AIResponse: + # OpenAI-specific implementation +``` + +### 2. Factory Pattern (AIService) + +Creates the appropriate provider based on configuration. + +```python +class AIService: + def __init__(self): + self.provider = self._create_provider() + + def _create_provider(self) -> AIProvider: + match settings.ai_provider: + case "openai": return OpenAIProvider() + case "anthropic": return AnthropicProvider() + # ... +``` + +### 3. Repository Pattern (Services) + +Services encapsulate data access logic. + +```python +class UserService: + @staticmethod + async def get_user(session: AsyncSession, discord_id: int) -> User | None: + result = await session.execute( + select(User).where(User.discord_id == discord_id) + ) + return result.scalar_one_or_none() +``` + +### 4. Cog Pattern (Discord.py) + +Modular command grouping. + +```python +class MemoryCog(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @commands.command(name="setname") + async def set_name(self, ctx, *, name: str): + # Command implementation +``` + +### 5. Graceful Degradation + +System works with reduced functionality when components unavailable. + +```python +# Database fallback +if settings.database_url: + conversation_manager = PersistentConversationManager() +else: + conversation_manager = ConversationManager() # In-memory + +# Feature toggles +if settings.living_ai_enabled and settings.mood_enabled: + mood = await MoodService.get_current_mood(session, guild_id) +else: + mood = None +``` + +--- + +## Component Interaction Diagram + +``` + ┌──────────────────────────────────────────┐ + │ User Message │ + └──────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ AIChatCog │ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ +│ │ UserService │ │ Conversation│ │ SearXNG │ │ AIService │ │ +│ │ │ │ Manager │ │ Service │ │ │ │ +│ │ get_user() │ │ get_history │ │ search() │ │ chat() ──────────┐ │ │ +│ │ get_context │ │ add_message │ │ │ │ │ │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ └──────────────────│──┘ │ +│ │ │ +│ ┌────────────────────────────────────────────────────────────────────┼────┐ │ +│ │ Living AI Services │ │ │ +│ │ │ │ │ +│ │ ┌───────────┐ ┌──────────────┐ ┌───────────────┐ ┌────────────┐ │ │ │ +│ │ │ MoodSvc │ │ Relationship │ │ CommStyleSvc │ │ OpinionSvc │ │ │ │ +│ │ │ │ │ Svc │ │ │ │ │ │ │ │ +│ │ │get_mood() │ │get_relation()│ │ get_style() │ │get_opinions│ │ │ │ +│ │ │update() │ │record() │ │ record() │ │ update() │ │ │ │ +│ │ └───────────┘ └──────────────┘ └───────────────┘ └────────────┘ │ │ │ +│ │ │ │ │ +│ │ ┌───────────────┐ ┌────────────────┐ ┌───────────────────────┐ │ │ │ +│ │ │ FactExtract │ │ ProactiveSvc │ │ SelfAwarenessSvc │ │ │ │ +│ │ │ │ │ │ │ │ │ │ │ +│ │ │ extract() │ │ detect_events()│ │ get_stats() │ │ │ │ +│ │ └───────────────┘ └────────────────┘ └───────────────────────┘ │ │ │ +│ └────────────────────────────────────────────────────────────────────│────┘ │ +│ │ │ +└────────────────────────────────────────────────────────────────────────│──────┘ + │ + ▼ + ┌──────────────────────────────────────────────┐ + │ AI Provider │ + │ (OpenAI / Anthropic / Gemini / OpenRouter) │ + └──────────────────────────────────────────────┘ + │ + ▼ + ┌──────────────────────────────────────────────┐ + │ Response │ + └──────────────────────────────────────────────┘ +``` + +--- + +## Next Steps + +- [Living AI System](living-ai/README.md) - Deep dive into the personality system +- [Services Reference](services/README.md) - Detailed API documentation +- [Database Schema](database.md) - Complete schema documentation +- [Configuration Reference](configuration.md) - All configuration options +- [Developer Guides](guides/README.md) - How to extend the system diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..7d59a84 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,637 @@ +# Configuration Reference + +Complete reference for all environment variables and configuration options. + +## Table of Contents + +- [Overview](#overview) +- [Required Configuration](#required-configuration) +- [AI Provider Configuration](#ai-provider-configuration) +- [Bot Identity](#bot-identity) +- [Database Configuration](#database-configuration) +- [Web Search (SearXNG)](#web-search-searxng) +- [Living AI Configuration](#living-ai-configuration) +- [Command Toggles](#command-toggles) +- [Logging Configuration](#logging-configuration) +- [Example .env File](#example-env-file) + +--- + +## Overview + +Configuration is managed via environment variables using [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/). + +**Loading Order:** +1. Environment variables +2. `.env` file in project root + +**Case Sensitivity:** Variable names are case-insensitive. + +--- + +## Required Configuration + +### DISCORD_TOKEN + +```bash +DISCORD_TOKEN=your_discord_bot_token +``` + +**Required.** Your Discord bot token from the [Discord Developer Portal](https://discord.com/developers/applications). + +### API Key (one required) + +At least one API key is required based on your chosen `AI_PROVIDER`: + +```bash +# For OpenAI +OPENAI_API_KEY=sk-... + +# For OpenRouter +OPENROUTER_API_KEY=sk-or-... + +# For Anthropic +ANTHROPIC_API_KEY=sk-ant-... + +# For Gemini +GEMINI_API_KEY=AIza... +``` + +--- + +## AI Provider Configuration + +### AI_PROVIDER + +```bash +AI_PROVIDER=openai +``` + +**Default:** `openai` +**Options:** `openai`, `openrouter`, `anthropic`, `gemini` + +Which AI provider to use for generating responses. + +### AI_MODEL + +```bash +AI_MODEL=gpt-4o +``` + +**Default:** `gpt-4o` + +The model to use. Depends on your provider: + +| Provider | Example Models | +|----------|----------------| +| openai | `gpt-4o`, `gpt-4o-mini`, `gpt-4-turbo` | +| openrouter | `anthropic/claude-3.5-sonnet`, `google/gemini-pro` | +| anthropic | `claude-3-5-sonnet-20241022`, `claude-3-opus-20240229` | +| gemini | `gemini-pro`, `gemini-1.5-pro` | + +### AI_MAX_TOKENS + +```bash +AI_MAX_TOKENS=1024 +``` + +**Default:** `1024` +**Range:** 100-4096 + +Maximum tokens in AI response. + +### AI_TEMPERATURE + +```bash +AI_TEMPERATURE=0.7 +``` + +**Default:** `0.7` +**Range:** 0.0-2.0 + +Sampling temperature. Higher = more creative, lower = more focused. + +--- + +## Bot Identity + +### BOT_NAME + +```bash +BOT_NAME=Daemon +``` + +**Default:** `AI Bot` + +The bot's display name used in system prompts. + +### BOT_PERSONALITY + +```bash +BOT_PERSONALITY=friendly, witty, and helpful +``` + +**Default:** `helpful and friendly` + +Personality description used in the default system prompt. + +### BOT_DESCRIPTION + +```bash +BOT_DESCRIPTION=I'm Daemon, your friendly AI companion! +``` + +**Default:** `I'm an AI assistant here to help you.` + +Description shown when bot is mentioned without a message. + +### BOT_STATUS + +```bash +BOT_STATUS=for @mentions +``` + +**Default:** `for mentions` + +Discord status message (shown as "Watching ..."). + +### SYSTEM_PROMPT + +```bash +SYSTEM_PROMPT=You are a helpful AI assistant named Daemon. Be friendly and concise. +``` + +**Default:** `None` (auto-generated) + +Custom system prompt. If not set, one is generated from `BOT_NAME` and `BOT_PERSONALITY`. + +--- + +## Database Configuration + +### DATABASE_URL + +```bash +DATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/daemon_boyfriend +``` + +**Default:** `None` (in-memory mode) + +PostgreSQL connection URL. Format: `postgresql+asyncpg://user:pass@host:port/database` + +If not set, the bot runs in in-memory mode without persistence. + +### DATABASE_ECHO + +```bash +DATABASE_ECHO=false +``` + +**Default:** `false` + +Log all SQL statements. Useful for debugging. + +### DATABASE_POOL_SIZE + +```bash +DATABASE_POOL_SIZE=5 +``` + +**Default:** `5` +**Range:** 1-20 + +Number of connections in the pool. + +### DATABASE_MAX_OVERFLOW + +```bash +DATABASE_MAX_OVERFLOW=10 +``` + +**Default:** `10` +**Range:** 0-30 + +Maximum connections beyond the pool size. + +--- + +## Web Search (SearXNG) + +### SEARXNG_URL + +```bash +SEARXNG_URL=http://localhost:8888 +``` + +**Default:** `None` (disabled) + +URL of your SearXNG instance for web search capability. + +### SEARXNG_ENABLED + +```bash +SEARXNG_ENABLED=true +``` + +**Default:** `true` + +Enable/disable web search even if URL is configured. + +### SEARXNG_MAX_RESULTS + +```bash +SEARXNG_MAX_RESULTS=5 +``` + +**Default:** `5` +**Range:** 1-20 + +Maximum search results to fetch per query. + +--- + +## Living AI Configuration + +### Master Switch + +#### LIVING_AI_ENABLED + +```bash +LIVING_AI_ENABLED=true +``` + +**Default:** `true` + +Master switch for all Living AI features. When disabled, the bot acts as a basic chatbot. + +--- + +### Feature Toggles + +#### MOOD_ENABLED + +```bash +MOOD_ENABLED=true +``` + +**Default:** `true` + +Enable the mood system (valence-arousal emotional model). + +#### RELATIONSHIP_ENABLED + +```bash +RELATIONSHIP_ENABLED=true +``` + +**Default:** `true` + +Enable relationship tracking (0-100 score, 5 levels). + +#### FACT_EXTRACTION_ENABLED + +```bash +FACT_EXTRACTION_ENABLED=true +``` + +**Default:** `true` + +Enable autonomous fact extraction from conversations. + +#### FACT_EXTRACTION_RATE + +```bash +FACT_EXTRACTION_RATE=0.3 +``` + +**Default:** `0.3` +**Range:** 0.0-1.0 + +Probability of attempting fact extraction for each message. Lower = fewer API calls. + +#### PROACTIVE_ENABLED + +```bash +PROACTIVE_ENABLED=true +``` + +**Default:** `true` + +Enable proactive messages (birthdays, follow-ups, reminders). + +#### CROSS_USER_ENABLED + +```bash +CROSS_USER_ENABLED=false +``` + +**Default:** `false` + +Enable cross-user memory associations. **Privacy-sensitive** - allows linking facts between users. + +#### OPINION_FORMATION_ENABLED + +```bash +OPINION_FORMATION_ENABLED=true +``` + +**Default:** `true` + +Enable bot opinion formation on topics. + +#### STYLE_LEARNING_ENABLED + +```bash +STYLE_LEARNING_ENABLED=true +``` + +**Default:** `true` + +Enable learning user communication style preferences. + +--- + +### Mood System Settings + +#### MOOD_DECAY_RATE + +```bash +MOOD_DECAY_RATE=0.1 +``` + +**Default:** `0.1` +**Range:** 0.0-1.0 + +How fast mood returns to neutral per hour. + +| Value | Effect | +|-------|--------| +| 0.0 | Mood never decays | +| 0.1 | Full decay in ~10 hours | +| 0.5 | Full decay in ~2 hours | +| 1.0 | Full decay in ~1 hour | + +--- + +### Conversation Settings + +#### MAX_CONVERSATION_HISTORY + +```bash +MAX_CONVERSATION_HISTORY=20 +``` + +**Default:** `20` + +Maximum messages to keep in conversation history per user. + +#### CONVERSATION_TIMEOUT_MINUTES + +```bash +CONVERSATION_TIMEOUT_MINUTES=60 +``` + +**Default:** `60` +**Range:** 5-1440 + +Minutes of inactivity before starting a new conversation. + +--- + +## Command Toggles + +### Master Switch + +#### COMMANDS_ENABLED + +```bash +COMMANDS_ENABLED=true +``` + +**Default:** `true` + +Master switch for all commands. When disabled, no commands work. + +--- + +### Individual Commands + +| Variable | Command | Default | Description | +|----------|---------|---------|-------------| +| `CMD_RELATIONSHIP_ENABLED` | `!relationship` | `true` | View relationship status | +| `CMD_MOOD_ENABLED` | `!mood` | `true` | View bot's current mood | +| `CMD_BOTSTATS_ENABLED` | `!botstats` | `true` | View bot statistics | +| `CMD_OURHISTORY_ENABLED` | `!ourhistory` | `true` | View history with the bot | +| `CMD_BIRTHDAY_ENABLED` | `!birthday` | `true` | Set birthday | +| `CMD_REMEMBER_ENABLED` | `!remember` | `true` | Tell bot a fact | +| `CMD_SETNAME_ENABLED` | `!setname` | `true` | Set custom name | +| `CMD_WHATDOYOUKNOW_ENABLED` | `!whatdoyouknow` | `true` | View known facts | +| `CMD_FORGETME_ENABLED` | `!forgetme` | `true` | Delete all facts | + +**Example:** + +```bash +# Disable only the forgetme command +CMD_FORGETME_ENABLED=false +``` + +--- + +## Logging Configuration + +### LOG_LEVEL + +```bash +LOG_LEVEL=INFO +``` + +**Default:** `INFO` +**Options:** `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` + +Logging verbosity level. + +### LOG_FILE + +```bash +LOG_FILE=/var/log/daemon_boyfriend.log +``` + +**Default:** `None` (console only) + +Path to log file. If not set, logs only to console. + +### LOG_FORMAT + +```bash +LOG_FORMAT=%(asctime)s - %(name)s - %(levelname)s - %(message)s +``` + +**Default:** `%(asctime)s - %(name)s - %(levelname)s - %(message)s` + +Python logging format string. + +--- + +## Example .env File + +```bash +# ============================================================================= +# DAEMON BOYFRIEND CONFIGURATION +# ============================================================================= + +# ----------------------------------------------------------------------------- +# REQUIRED +# ----------------------------------------------------------------------------- + +# Discord bot token (from Discord Developer Portal) +DISCORD_TOKEN=your_discord_bot_token_here + +# AI Provider API Key (choose one based on AI_PROVIDER) +OPENAI_API_KEY=sk-your-openai-key-here +# OPENROUTER_API_KEY=sk-or-your-openrouter-key +# ANTHROPIC_API_KEY=sk-ant-your-anthropic-key +# GEMINI_API_KEY=AIza-your-gemini-key + +# ----------------------------------------------------------------------------- +# AI PROVIDER +# ----------------------------------------------------------------------------- + +# Provider: openai, openrouter, anthropic, gemini +AI_PROVIDER=openai + +# Model to use (depends on provider) +AI_MODEL=gpt-4o + +# Response settings +AI_MAX_TOKENS=1024 +AI_TEMPERATURE=0.7 + +# ----------------------------------------------------------------------------- +# BOT IDENTITY +# ----------------------------------------------------------------------------- + +BOT_NAME=Daemon +BOT_PERSONALITY=friendly, witty, and helpful +BOT_DESCRIPTION=I'm Daemon, your AI companion! +BOT_STATUS=for @mentions + +# Optional: Override the entire system prompt +# SYSTEM_PROMPT=You are a helpful AI assistant named Daemon. + +# ----------------------------------------------------------------------------- +# DATABASE (Optional - runs in-memory if not set) +# ----------------------------------------------------------------------------- + +DATABASE_URL=postgresql+asyncpg://daemon:password@localhost:5432/daemon_boyfriend +# DATABASE_ECHO=false +# DATABASE_POOL_SIZE=5 +# DATABASE_MAX_OVERFLOW=10 + +# ----------------------------------------------------------------------------- +# WEB SEARCH (Optional) +# ----------------------------------------------------------------------------- + +# SEARXNG_URL=http://localhost:8888 +# SEARXNG_ENABLED=true +# SEARXNG_MAX_RESULTS=5 + +# ----------------------------------------------------------------------------- +# LIVING AI FEATURES +# ----------------------------------------------------------------------------- + +# Master switch +LIVING_AI_ENABLED=true + +# Individual features +MOOD_ENABLED=true +RELATIONSHIP_ENABLED=true +FACT_EXTRACTION_ENABLED=true +FACT_EXTRACTION_RATE=0.3 +PROACTIVE_ENABLED=true +CROSS_USER_ENABLED=false +OPINION_FORMATION_ENABLED=true +STYLE_LEARNING_ENABLED=true + +# Mood settings +MOOD_DECAY_RATE=0.1 + +# Conversation settings +MAX_CONVERSATION_HISTORY=20 +CONVERSATION_TIMEOUT_MINUTES=60 + +# ----------------------------------------------------------------------------- +# COMMAND TOGGLES +# ----------------------------------------------------------------------------- + +COMMANDS_ENABLED=true +# CMD_RELATIONSHIP_ENABLED=true +# CMD_MOOD_ENABLED=true +# CMD_BOTSTATS_ENABLED=true +# CMD_OURHISTORY_ENABLED=true +# CMD_BIRTHDAY_ENABLED=true +# CMD_REMEMBER_ENABLED=true +# CMD_SETNAME_ENABLED=true +# CMD_WHATDOYOUKNOW_ENABLED=true +# CMD_FORGETME_ENABLED=true + +# ----------------------------------------------------------------------------- +# LOGGING +# ----------------------------------------------------------------------------- + +LOG_LEVEL=INFO +# LOG_FILE=/var/log/daemon_boyfriend.log +``` + +--- + +## Configuration Profiles + +### Minimal Setup + +```bash +DISCORD_TOKEN=your_token +OPENAI_API_KEY=your_key +``` + +### Production Setup + +```bash +DISCORD_TOKEN=your_token +OPENAI_API_KEY=your_key +AI_MODEL=gpt-4o +DATABASE_URL=postgresql+asyncpg://user:pass@db:5432/daemon +LOG_LEVEL=WARNING +``` + +### Development Setup + +```bash +DISCORD_TOKEN=your_token +OPENAI_API_KEY=your_key +AI_MODEL=gpt-4o-mini # Cheaper model +DATABASE_ECHO=true +LOG_LEVEL=DEBUG +``` + +### Privacy-Focused Setup + +```bash +DISCORD_TOKEN=your_token +OPENAI_API_KEY=your_key +FACT_EXTRACTION_ENABLED=false +CROSS_USER_ENABLED=false +CMD_WHATDOYOUKNOW_ENABLED=true +CMD_FORGETME_ENABLED=true +``` + +--- + +## Validation + +Settings are validated at startup using Pydantic: + +- **Type checking:** Ensures correct types +- **Range validation:** Enforces min/max values +- **Required fields:** Ensures essential config is present + +If validation fails, the bot will not start and will show an error message indicating what's wrong. diff --git a/docs/database.md b/docs/database.md new file mode 100644 index 0000000..761e79d --- /dev/null +++ b/docs/database.md @@ -0,0 +1,570 @@ +# Database Schema Guide + +This document describes the database schema used by Daemon Boyfriend. + +## Table of Contents + +- [Overview](#overview) +- [Core Tables](#core-tables) +- [Living AI Tables](#living-ai-tables) +- [Indexes](#indexes) +- [Relationships](#relationships) +- [Schema Diagram](#schema-diagram) + +--- + +## Overview + +### Database Support + +- **Primary:** PostgreSQL with async support (`asyncpg`) +- **Fallback:** In-memory (no persistence) +- **Testing:** SQLite (via `aiosqlite`) + +### Key Features + +- **PortableJSON:** Custom type for PostgreSQL JSONB / SQLite JSON compatibility +- **UTC Timestamps:** All timestamps are stored as timezone-aware UTC +- **Soft Deletes:** Many tables use `is_active` flag instead of hard deletes +- **Cascade Deletes:** Foreign keys cascade on user/conversation deletion + +### Connection Configuration + +```bash +# PostgreSQL connection string +DATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/daemon_boyfriend + +# Optional settings +DATABASE_ECHO=false # Log SQL queries +DATABASE_POOL_SIZE=5 # Connection pool size +DATABASE_MAX_OVERFLOW=10 # Max connections beyond pool +``` + +--- + +## Core Tables + +### users + +Stores Discord user information. + +| Column | Type | Description | +|--------|------|-------------| +| `id` | BIGSERIAL | Primary key | +| `discord_id` | BIGINT | Discord user ID (unique) | +| `discord_username` | VARCHAR(255) | Discord username | +| `discord_display_name` | VARCHAR(255) | Discord display name | +| `custom_name` | VARCHAR(255) | Custom name set by user/admin | +| `first_seen_at` | TIMESTAMPTZ | When user was first seen | +| `last_seen_at` | TIMESTAMPTZ | When user was last seen | +| `is_active` | BOOLEAN | Whether user is active | +| `created_at` | TIMESTAMPTZ | Record creation time | +| `updated_at` | TIMESTAMPTZ | Record update time | + +**Usage:** +```python +user = await user_service.get_or_create_user( + discord_id=123456789, + username="john_doe", + display_name="John" +) +print(user.display_name) # Returns custom_name or discord_display_name +``` + +--- + +### user_preferences + +Key-value storage for user preferences. + +| Column | Type | Description | +|--------|------|-------------| +| `id` | BIGSERIAL | Primary key | +| `user_id` | BIGINT | Foreign key to users | +| `preference_key` | VARCHAR(100) | Preference name | +| `preference_value` | TEXT | Preference value | +| `created_at` | TIMESTAMPTZ | Record creation time | +| `updated_at` | TIMESTAMPTZ | Record update time | + +**Unique Constraint:** `(user_id, preference_key)` + +--- + +### user_facts + +Facts the bot knows about users. + +| Column | Type | Description | +|--------|------|-------------| +| `id` | BIGSERIAL | Primary key | +| `user_id` | BIGINT | Foreign key to users | +| `fact_type` | VARCHAR(50) | Type (hobby, work, family, etc.) | +| `fact_content` | TEXT | The fact itself | +| `confidence` | FLOAT | 0.0-1.0 confidence level | +| `source` | VARCHAR(50) | How learned (conversation, auto_extraction, manual) | +| `is_active` | BOOLEAN | Whether fact is active | +| `learned_at` | TIMESTAMPTZ | When fact was learned | +| `last_referenced_at` | TIMESTAMPTZ | When fact was last used | +| `category` | VARCHAR(50) | Category (same as fact_type) | +| `importance` | FLOAT | 0.0-1.0 importance level | +| `temporal_relevance` | VARCHAR(20) | past/present/future/timeless | +| `expiry_date` | TIMESTAMPTZ | When fact expires (optional) | +| `extracted_from_message_id` | BIGINT | Source Discord message ID | +| `extraction_context` | TEXT | Context of extraction | +| `created_at` | TIMESTAMPTZ | Record creation time | +| `updated_at` | TIMESTAMPTZ | Record update time | + +**Fact Types:** +- `hobby` - Activities, interests +- `work` - Job, career +- `family` - Family members +- `preference` - Likes, dislikes +- `location` - Places +- `event` - Life events +- `relationship` - Personal relationships +- `general` - Other facts + +--- + +### guilds + +Discord server (guild) information. + +| Column | Type | Description | +|--------|------|-------------| +| `id` | BIGSERIAL | Primary key | +| `discord_id` | BIGINT | Discord guild ID (unique) | +| `name` | VARCHAR(255) | Guild name | +| `joined_at` | TIMESTAMPTZ | When bot joined | +| `is_active` | BOOLEAN | Whether bot is active in guild | +| `settings` | JSONB | Guild-specific settings | +| `created_at` | TIMESTAMPTZ | Record creation time | +| `updated_at` | TIMESTAMPTZ | Record update time | + +--- + +### guild_members + +User membership in guilds. + +| Column | Type | Description | +|--------|------|-------------| +| `id` | BIGSERIAL | Primary key | +| `guild_id` | BIGINT | Foreign key to guilds | +| `user_id` | BIGINT | Foreign key to users | +| `guild_nickname` | VARCHAR(255) | Nickname in this guild | +| `roles` | TEXT[] | Array of role names | +| `joined_guild_at` | TIMESTAMPTZ | When user joined guild | +| `created_at` | TIMESTAMPTZ | Record creation time | +| `updated_at` | TIMESTAMPTZ | Record update time | + +**Unique Constraint:** `(guild_id, user_id)` + +--- + +### conversations + +Conversation sessions between users and the bot. + +| Column | Type | Description | +|--------|------|-------------| +| `id` | BIGSERIAL | Primary key | +| `user_id` | BIGINT | Foreign key to users | +| `guild_id` | BIGINT | Guild ID (nullable for DMs) | +| `channel_id` | BIGINT | Discord channel ID | +| `started_at` | TIMESTAMPTZ | When conversation started | +| `last_message_at` | TIMESTAMPTZ | Last message timestamp | +| `message_count` | INTEGER | Number of messages | +| `is_active` | BOOLEAN | Whether conversation is active | +| `created_at` | TIMESTAMPTZ | Record creation time | +| `updated_at` | TIMESTAMPTZ | Record update time | + +**Conversation Timeout:** +A new conversation starts after 60 minutes of inactivity. + +--- + +### messages + +Individual messages in conversations. + +| Column | Type | Description | +|--------|------|-------------| +| `id` | BIGSERIAL | Primary key | +| `conversation_id` | BIGINT | Foreign key to conversations | +| `user_id` | BIGINT | Foreign key to users | +| `discord_message_id` | BIGINT | Discord message ID | +| `role` | VARCHAR(20) | user/assistant/system | +| `content` | TEXT | Message content | +| `has_images` | BOOLEAN | Whether message has images | +| `image_urls` | JSONB | Array of image URLs | +| `token_count` | INTEGER | Token count (estimated) | +| `created_at` | TIMESTAMPTZ | Record creation time | +| `updated_at` | TIMESTAMPTZ | Record update time | + +--- + +## Living AI Tables + +### bot_states + +Per-guild bot state (mood, statistics). + +| Column | Type | Description | +|--------|------|-------------| +| `id` | BIGSERIAL | Primary key | +| `guild_id` | BIGINT | Guild ID (unique, NULL = global) | +| `mood_valence` | FLOAT | -1.0 to 1.0 (sad to happy) | +| `mood_arousal` | FLOAT | -1.0 to 1.0 (calm to excited) | +| `mood_updated_at` | TIMESTAMPTZ | When mood was last updated | +| `total_messages_sent` | INTEGER | Lifetime message count | +| `total_facts_learned` | INTEGER | Facts extracted count | +| `total_users_known` | INTEGER | Unique users count | +| `first_activated_at` | TIMESTAMPTZ | Bot "birth date" | +| `preferences` | JSONB | Bot preferences | +| `created_at` | TIMESTAMPTZ | Record creation time | +| `updated_at` | TIMESTAMPTZ | Record update time | + +--- + +### bot_opinions + +Bot's opinions on topics. + +| Column | Type | Description | +|--------|------|-------------| +| `id` | BIGSERIAL | Primary key | +| `guild_id` | BIGINT | Guild ID (NULL = global) | +| `topic` | VARCHAR(255) | Topic name (lowercase) | +| `sentiment` | FLOAT | -1.0 to 1.0 | +| `interest_level` | FLOAT | 0.0 to 1.0 | +| `discussion_count` | INTEGER | Times discussed | +| `reasoning` | TEXT | AI explanation (optional) | +| `formed_at` | TIMESTAMPTZ | When opinion formed | +| `last_reinforced_at` | TIMESTAMPTZ | When last discussed | +| `created_at` | TIMESTAMPTZ | Record creation time | +| `updated_at` | TIMESTAMPTZ | Record update time | + +**Unique Constraint:** `(guild_id, topic)` + +--- + +### user_relationships + +Relationship depth with each user. + +| Column | Type | Description | +|--------|------|-------------| +| `id` | BIGSERIAL | Primary key | +| `user_id` | BIGINT | Foreign key to users | +| `guild_id` | BIGINT | Guild ID (NULL = global) | +| `relationship_score` | FLOAT | 0-100 score | +| `total_interactions` | INTEGER | Total interaction count | +| `positive_interactions` | INTEGER | Positive interaction count | +| `negative_interactions` | INTEGER | Negative interaction count | +| `avg_message_length` | FLOAT | Average message length | +| `conversation_depth_avg` | FLOAT | Average conversation turns | +| `shared_references` | JSONB | Inside jokes, nicknames, etc. | +| `first_interaction_at` | TIMESTAMPTZ | First interaction | +| `last_interaction_at` | TIMESTAMPTZ | Last interaction | +| `created_at` | TIMESTAMPTZ | Record creation time | +| `updated_at` | TIMESTAMPTZ | Record update time | + +**Unique Constraint:** `(user_id, guild_id)` + +**Relationship Score Levels:** +- 0-20: Stranger +- 21-40: Acquaintance +- 41-60: Friend +- 61-80: Good Friend +- 81-100: Close Friend + +--- + +### user_communication_styles + +Learned communication preferences per user. + +| Column | Type | Description | +|--------|------|-------------| +| `id` | BIGSERIAL | Primary key | +| `user_id` | BIGINT | Foreign key to users (unique) | +| `preferred_length` | VARCHAR(20) | short/medium/long | +| `preferred_formality` | FLOAT | 0.0 (casual) to 1.0 (formal) | +| `emoji_affinity` | FLOAT | 0.0 (none) to 1.0 (lots) | +| `humor_affinity` | FLOAT | 0.0 (serious) to 1.0 (playful) | +| `detail_preference` | FLOAT | 0.0 (concise) to 1.0 (detailed) | +| `engagement_signals` | JSONB | Engagement signal data | +| `samples_collected` | INTEGER | Number of samples analyzed | +| `confidence` | FLOAT | 0.0-1.0 confidence level | +| `created_at` | TIMESTAMPTZ | Record creation time | +| `updated_at` | TIMESTAMPTZ | Record update time | + +--- + +### scheduled_events + +Proactive events (birthdays, follow-ups). + +| Column | Type | Description | +|--------|------|-------------| +| `id` | BIGSERIAL | Primary key | +| `user_id` | BIGINT | Foreign key to users | +| `guild_id` | BIGINT | Target guild ID | +| `channel_id` | BIGINT | Target channel ID | +| `event_type` | VARCHAR(50) | birthday/follow_up/reminder | +| `trigger_at` | TIMESTAMPTZ | When to trigger | +| `title` | VARCHAR(255) | Event title | +| `context` | JSONB | Additional context | +| `is_recurring` | BOOLEAN | Whether event recurs | +| `recurrence_rule` | VARCHAR(100) | yearly/monthly/weekly/etc. | +| `status` | VARCHAR(20) | pending/triggered/cancelled | +| `triggered_at` | TIMESTAMPTZ | When event was triggered | +| `created_at` | TIMESTAMPTZ | Record creation time | +| `updated_at` | TIMESTAMPTZ | Record update time | + +--- + +### fact_associations + +Cross-user fact associations. + +| Column | Type | Description | +|--------|------|-------------| +| `id` | BIGSERIAL | Primary key | +| `fact_id_1` | BIGINT | Foreign key to user_facts | +| `fact_id_2` | BIGINT | Foreign key to user_facts | +| `association_type` | VARCHAR(50) | shared_interest/same_location/etc. | +| `strength` | FLOAT | 0.0-1.0 association strength | +| `discovered_at` | TIMESTAMPTZ | When discovered | +| `created_at` | TIMESTAMPTZ | Record creation time | + +**Unique Constraint:** `(fact_id_1, fact_id_2)` + +--- + +### mood_history + +Historical mood changes. + +| Column | Type | Description | +|--------|------|-------------| +| `id` | BIGSERIAL | Primary key | +| `guild_id` | BIGINT | Guild ID | +| `valence` | FLOAT | Valence at this point | +| `arousal` | FLOAT | Arousal at this point | +| `trigger_type` | VARCHAR(50) | conversation/time_decay/event | +| `trigger_user_id` | BIGINT | User who triggered (optional) | +| `trigger_description` | TEXT | Description of trigger | +| `recorded_at` | TIMESTAMPTZ | When recorded | +| `created_at` | TIMESTAMPTZ | Record creation time | +| `updated_at` | TIMESTAMPTZ | Record update time | + +--- + +## Indexes + +All indexes are created with `IF NOT EXISTS` for idempotency. + +### Core Table Indexes + +| Table | Index | Columns | +|-------|-------|---------| +| users | ix_users_discord_id | discord_id | +| users | ix_users_last_seen_at | last_seen_at | +| user_preferences | ix_user_preferences_user_id | user_id | +| user_facts | ix_user_facts_user_id | user_id | +| user_facts | ix_user_facts_fact_type | fact_type | +| user_facts | ix_user_facts_is_active | is_active | +| guilds | ix_guilds_discord_id | discord_id | +| guild_members | ix_guild_members_guild_id | guild_id | +| guild_members | ix_guild_members_user_id | user_id | +| conversations | ix_conversations_user_id | user_id | +| conversations | ix_conversations_channel_id | channel_id | +| conversations | ix_conversations_last_message_at | last_message_at | +| conversations | ix_conversations_is_active | is_active | +| messages | ix_messages_conversation_id | conversation_id | +| messages | ix_messages_user_id | user_id | +| messages | ix_messages_created_at | created_at | + +### Living AI Table Indexes + +| Table | Index | Columns | +|-------|-------|---------| +| bot_states | ix_bot_states_guild_id | guild_id | +| bot_opinions | ix_bot_opinions_guild_id | guild_id | +| bot_opinions | ix_bot_opinions_topic | topic | +| user_relationships | ix_user_relationships_user_id | user_id | +| user_relationships | ix_user_relationships_guild_id | guild_id | +| user_communication_styles | ix_user_communication_styles_user_id | user_id | +| scheduled_events | ix_scheduled_events_user_id | user_id | +| scheduled_events | ix_scheduled_events_trigger_at | trigger_at | +| scheduled_events | ix_scheduled_events_status | status | +| fact_associations | ix_fact_associations_fact_id_1 | fact_id_1 | +| fact_associations | ix_fact_associations_fact_id_2 | fact_id_2 | +| mood_history | ix_mood_history_guild_id | guild_id | +| mood_history | ix_mood_history_recorded_at | recorded_at | + +--- + +## Relationships + +### Entity Relationship Diagram + +``` +┌──────────────────┐ ┌──────────────────┐ +│ users │ │ guilds │ +├──────────────────┤ ├──────────────────┤ +│ id (PK) │◄────┐ │ id (PK) │◄────┐ +│ discord_id │ │ │ discord_id │ │ +│ discord_username │ │ │ name │ │ +│ custom_name │ │ │ settings │ │ +└──────────────────┘ │ └──────────────────┘ │ + │ │ │ │ + │ │ │ │ + ▼ │ ▼ │ +┌──────────────────┐ │ ┌──────────────────┐ │ +│ user_facts │ │ │ guild_members │ │ +├──────────────────┤ │ ├──────────────────┤ │ +│ id (PK) │ │ │ id (PK) │ │ +│ user_id (FK) ────┼─────┘ │ guild_id (FK) ───┼─────┘ +│ fact_type │ │ user_id (FK) ────┼─────┐ +│ fact_content │ │ guild_nickname │ │ +└──────────────────┘ └──────────────────┘ │ + ▲ │ + │ │ + │ │ +┌────────┴─────────┐ ┌──────────────────┐ │ +│fact_associations │ │ conversations │ │ +├──────────────────┤ ├──────────────────┤ │ +│ id (PK) │ │ id (PK) │◄────┤ +│ fact_id_1 (FK) │ │ user_id (FK) ────┼─────┘ +│ fact_id_2 (FK) │ │ guild_id │ +│ association_type │ │ channel_id │ +└──────────────────┘ └──────────────────┘ + │ + │ + ▼ + ┌──────────────────┐ + │ messages │ + ├──────────────────┤ + │ id (PK) │ + │ conversation_id │ + │ user_id (FK) │ + │ role │ + │ content │ + └──────────────────┘ +``` + +### Living AI Relationships + +``` +┌──────────────────┐ +│ users │ +├──────────────────┤ +│ id (PK) │◄─────────────────────────────────────┐ +└──────────────────┘ │ + │ │ + │ │ + ┌────┴────────┬───────────────┬───────────────┐ │ + │ │ │ │ │ + ▼ ▼ ▼ ▼ │ +┌────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ +│user_ │ │user_comm │ │scheduled │ │mood_ │ │ +│relation│ │styles │ │events │ │history │ │ +│ships │ │ │ │ │ │ │ │ +├────────┤ ├────────────┤ ├────────────┤ ├────────────┤ │ +│user_id │ │user_id │ │user_id │ │trigger_ │ │ +│guild_id│ │preferred_ │ │guild_id │ │user_id ────┼──┘ +│score │ │length │ │event_type │ │valence │ +│ │ │emoji │ │trigger_at │ │arousal │ +└────────┘ └────────────┘ └────────────┘ └────────────┘ + + +┌──────────────────┐ ┌──────────────────┐ +│ bot_states │ │ bot_opinions │ +├──────────────────┤ ├──────────────────┤ +│ guild_id (unique)│ │ guild_id │ +│ mood_valence │ │ topic │ +│ mood_arousal │ │ sentiment │ +│ total_messages │ │ interest_level │ +│ total_facts │ │ discussion_count │ +└──────────────────┘ └──────────────────┘ +``` + +--- + +## Schema Initialization + +### From Code + +```python +from daemon_boyfriend.services.database import db + +# Initialize connection +await db.init() + +# Create tables from schema.sql +await db.create_tables() +``` + +### From SQL + +```bash +# Create database +createdb daemon_boyfriend + +# Run schema +psql -U postgres -d daemon_boyfriend -f schema.sql +``` + +### Docker + +The `docker-compose.yml` handles database setup automatically: + +```bash +docker-compose up -d +``` + +--- + +## Migrations + +The project uses **Alembic** for migrations, but currently relies on `schema.sql` for initial setup. + +```bash +# Generate migration +alembic revision --autogenerate -m "description" + +# Run migrations +alembic upgrade head + +# Rollback +alembic downgrade -1 +``` + +--- + +## Type Compatibility + +### PortableJSON + +For JSONB (PostgreSQL) and JSON (SQLite) compatibility: + +```python +from daemon_boyfriend.models.base import PortableJSON + +class MyModel(Base): + settings = Column(PortableJSON, default={}) +``` + +### ensure_utc() + +Handles timezone-naive datetimes from SQLite: + +```python +from daemon_boyfriend.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 new file mode 100644 index 0000000..9d4abbf --- /dev/null +++ b/docs/guides/README.md @@ -0,0 +1,589 @@ +# Developer Guides + +Practical guides for extending and working with the Daemon Boyfriend codebase. + +## Table of Contents + +- [Getting Started](#getting-started) +- [Adding a New AI Provider](#adding-a-new-ai-provider) +- [Adding a New Command](#adding-a-new-command) +- [Adding a Living AI Feature](#adding-a-living-ai-feature) +- [Testing](#testing) +- [Deployment](#deployment) + +--- + +## Getting Started + +### Prerequisites + +- Python 3.11+ +- PostgreSQL (optional, for persistence) +- Discord bot token +- AI provider API key + +### Installation + +```bash +# Clone repository +git clone +cd daemon-boyfriend + +# Create virtual environment +python -m venv venv +source venv/bin/activate # Linux/Mac +# or: venv\Scripts\activate # Windows + +# Install dependencies +pip install -r requirements.txt + +# Install in development mode +pip install -e ".[dev]" +``` + +### Configuration + +```bash +# Copy example config +cp .env.example .env + +# Edit with your credentials +nano .env +``` + +Minimum required: +```bash +DISCORD_TOKEN=your_token +OPENAI_API_KEY=your_key +``` + +### Running + +```bash +# Run the bot +python -m daemon_boyfriend + +# Or with Docker +docker-compose up -d +``` + +--- + +## Adding a New AI Provider + +### Step 1: Create Provider Class + +Create `src/daemon_boyfriend/services/providers/new_provider.py`: + +```python +"""New Provider implementation.""" + +import logging +from typing import Any + +from .base import AIProvider, AIResponse, Message + +logger = logging.getLogger(__name__) + + +class NewProvider(AIProvider): + """Implementation for New Provider API.""" + + def __init__(self, api_key: str, model: str) -> None: + self._api_key = api_key + self._model = model + # Initialize client + self._client = NewProviderClient(api_key=api_key) + + @property + def provider_name(self) -> str: + return "new_provider" + + async def generate( + self, + messages: list[Message], + system_prompt: str | None = None, + max_tokens: int = 1024, + temperature: float = 0.7, + ) -> AIResponse: + """Generate a response from the model.""" + # Convert messages to provider format + formatted_messages = [] + + if system_prompt: + formatted_messages.append({ + "role": "system", + "content": system_prompt + }) + + for msg in messages: + formatted_messages.append({ + "role": msg.role, + "content": msg.content + }) + + # Call provider API + response = await self._client.chat.completions.create( + model=self._model, + messages=formatted_messages, + max_tokens=max_tokens, + temperature=temperature, + ) + + return AIResponse( + content=response.choices[0].message.content, + model=response.model, + usage={ + "prompt_tokens": response.usage.prompt_tokens, + "completion_tokens": response.usage.completion_tokens, + "total_tokens": response.usage.total_tokens, + } + ) +``` + +### Step 2: Export from providers/__init__.py + +```python +# In services/providers/__init__.py +from .new_provider import NewProvider + +__all__ = [ + # ... existing exports + "NewProvider", +] +``` + +### Step 3: Register in AIService + +```python +# In services/ai_service.py + +from .providers import NewProvider + +class AIService: + def _create_provider(self, provider_type, api_key, model): + providers = { + "openai": OpenAIProvider, + "openrouter": OpenRouterProvider, + "anthropic": AnthropicProvider, + "gemini": GeminiProvider, + "new_provider": NewProvider, # Add here + } + # ... +``` + +### Step 4: Add Configuration + +```python +# In config.py + +class Settings(BaseSettings): + ai_provider: Literal["openai", "openrouter", "anthropic", "gemini", "new_provider"] = Field( + "openai", description="Which AI provider to use" + ) + + new_provider_api_key: str | None = Field(None, description="New Provider API key") + + def get_api_key(self) -> str: + key_map = { + # ...existing... + "new_provider": self.new_provider_api_key, + } +``` + +### Step 5: Add Tests + +```python +# In tests/test_providers.py + +import pytest +from daemon_boyfriend.services.providers import NewProvider + +@pytest.mark.asyncio +async def test_new_provider(): + provider = NewProvider(api_key="test", model="test-model") + assert provider.provider_name == "new_provider" +``` + +--- + +## Adding a New Command + +### Step 1: Add to Existing Cog + +For simple commands, add to an existing cog: + +```python +# In cogs/memory.py + +@commands.command(name="newcommand") +async def new_command(self, ctx: commands.Context, *, argument: str): + """Description of what the command does.""" + if not settings.cmd_newcommand_enabled: + return + + async with db.session() as session: + user_service = UserService(session) + user = await user_service.get_or_create_user( + discord_id=ctx.author.id, + username=ctx.author.name, + display_name=ctx.author.display_name, + ) + + # Do something with the command + result = await self._process_command(user, argument) + + await ctx.send(f"Result: {result}") +``` + +### Step 2: Create a New Cog + +For complex features, create a new cog: + +```python +# In cogs/new_feature.py + +"""New Feature Cog.""" + +import logging + +import discord +from discord.ext import commands + +from daemon_boyfriend.config import settings +from daemon_boyfriend.services.database import db + +logger = logging.getLogger(__name__) + + +class NewFeatureCog(commands.Cog): + """Commands for the new feature.""" + + def __init__(self, bot: commands.Bot) -> None: + self.bot = bot + + @commands.command(name="newfeature") + async def new_feature(self, ctx: commands.Context, *, arg: str): + """New feature command.""" + if not settings.commands_enabled: + return + + # Command implementation + await ctx.send(f"New feature: {arg}") + + @commands.Cog.listener() + async def on_message(self, message: discord.Message): + """Optional: Listen to all messages.""" + if message.author.bot: + return + + # Process messages if needed + + +async def setup(bot: commands.Bot) -> None: + """Load the cog.""" + await bot.add_cog(NewFeatureCog(bot)) +``` + +Cogs are auto-loaded from the `cogs/` directory. + +### Step 3: Add Configuration Toggle + +```python +# In config.py + +class Settings(BaseSettings): + cmd_newfeature_enabled: bool = Field(True, description="Enable !newfeature command") +``` + +--- + +## Adding a Living AI Feature + +### Step 1: Create the Service + +```python +# In services/new_feature_service.py + +"""New Feature Service - description.""" + +import logging +from datetime import datetime, timezone + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from daemon_boyfriend.models import User + +logger = logging.getLogger(__name__) + + +class NewFeatureService: + """Manages the new feature.""" + + def __init__(self, session: AsyncSession) -> None: + self._session = session + + async def get_feature_data(self, user: User) -> dict: + """Get feature data for a user.""" + # Implementation + return {} + + async def update_feature(self, user: User, data: dict) -> None: + """Update feature data.""" + # Implementation + pass + + def get_prompt_modifier(self, data: dict) -> str: + """Generate prompt text for this feature.""" + if not data: + return "" + return f"[New Feature Context]\n{data}" +``` + +### Step 2: Create the Model (if needed) + +```python +# In models/living_ai.py (or new file) + +class NewFeatureData(Base): + """New feature data storage.""" + + __tablename__ = "new_feature_data" + + id = Column(BigInteger, primary_key=True, autoincrement=True) + user_id = Column(BigInteger, ForeignKey("users.id", ondelete="CASCADE"), nullable=False) + guild_id = Column(BigInteger, nullable=True) + + # Feature-specific fields + some_value = Column(Float, default=0.0) + some_dict = Column(PortableJSON, default={}) + + created_at = Column(DateTime(timezone=True), default=utc_now) + updated_at = Column(DateTime(timezone=True), default=utc_now, onupdate=utc_now) + + # Relationships + user = relationship("User", back_populates="new_feature_data") +``` + +### Step 3: Add to Schema + +```sql +-- In schema.sql + +CREATE TABLE IF NOT EXISTS new_feature_data ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + guild_id BIGINT, + some_value FLOAT DEFAULT 0.0, + some_dict JSONB DEFAULT '{}', + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(user_id, guild_id) +); + +CREATE INDEX IF NOT EXISTS ix_new_feature_data_user_id ON new_feature_data(user_id); +``` + +### Step 4: Add Configuration + +```python +# In config.py + +class Settings(BaseSettings): + new_feature_enabled: bool = Field(True, description="Enable new feature") +``` + +### Step 5: Integrate in AIChatCog + +```python +# In cogs/ai_chat.py + +from daemon_boyfriend.services.new_feature_service import NewFeatureService + +class AIChatCog(commands.Cog): + async def _build_enhanced_prompt(self, ...): + # ... existing code ... + + # Add new feature + if settings.new_feature_enabled: + new_feature_service = NewFeatureService(session) + feature_data = await new_feature_service.get_feature_data(user) + feature_modifier = new_feature_service.get_prompt_modifier(feature_data) + modifiers.append(feature_modifier) +``` + +--- + +## Testing + +### Running Tests + +```bash +# Install dev dependencies +pip install -e ".[dev]" + +# Run all tests +python -m pytest tests/ -v + +# Run with coverage +python -m pytest tests/ --cov=daemon_boyfriend --cov-report=term-missing + +# Run specific test file +python -m pytest tests/test_models.py -v + +# Run specific test class +python -m pytest tests/test_services.py::TestMoodService -v +``` + +### Writing Tests + +```python +# In tests/test_new_feature.py + +import pytest +from daemon_boyfriend.services.new_feature_service import NewFeatureService + + +class TestNewFeatureService: + """Tests for NewFeatureService.""" + + @pytest.mark.asyncio + async def test_get_feature_data(self, db_session, mock_user): + """Test getting feature data.""" + service = NewFeatureService(db_session) + + data = await service.get_feature_data(mock_user) + + assert data is not None + assert isinstance(data, dict) + + @pytest.mark.asyncio + async def test_update_feature(self, db_session, mock_user): + """Test updating feature data.""" + service = NewFeatureService(db_session) + + await service.update_feature(mock_user, {"key": "value"}) + + data = await service.get_feature_data(mock_user) + assert data.get("key") == "value" +``` + +### Using Fixtures + +```python +# In tests/conftest.py + +@pytest.fixture +async def db_session(): + """Provide a database session for testing.""" + # Uses SQLite in-memory for tests + async with get_test_session() as session: + yield session + +@pytest.fixture +def mock_user(db_session): + """Provide a mock user for testing.""" + user = User( + discord_id=123456789, + discord_username="test_user", + discord_display_name="Test User" + ) + db_session.add(user) + return user +``` + +--- + +## Deployment + +### Docker + +```bash +# Build and run +docker-compose up -d + +# View logs +docker-compose logs -f + +# Stop +docker-compose down +``` + +### Manual + +```bash +# Install +pip install -r requirements.txt + +# Run with process manager (e.g., systemd) +# Create /etc/systemd/system/daemon-boyfriend.service: + +[Unit] +Description=Daemon Boyfriend Discord Bot +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 +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +``` + +```bash +# Enable and start +sudo systemctl enable daemon-boyfriend +sudo systemctl start daemon-boyfriend +``` + +### Database Setup + +```bash +# Create database +sudo -u postgres createdb daemon_boyfriend + +# Run schema +psql -U postgres -d daemon_boyfriend -f schema.sql + +# Or let the bot create tables +# (tables are created automatically on first run) +``` + +--- + +## Best Practices + +### Code Style + +- Use type hints everywhere +- Follow PEP 8 +- Use async/await for all I/O +- Log appropriately (debug for routine, info for significant events) + +### Database + +- Always use async sessions +- Use transactions appropriately +- Index frequently queried columns +- Use soft deletes where appropriate + +### Testing + +- Test both happy path and error cases +- Use fixtures for common setup +- Mock external services (AI providers, Discord) +- Test async code with `@pytest.mark.asyncio` + +### Security + +- Never log sensitive data (tokens, passwords) +- Validate user input +- Use parameterized queries (SQLAlchemy handles this) +- Rate limit where appropriate diff --git a/docs/living-ai/README.md b/docs/living-ai/README.md new file mode 100644 index 0000000..c419699 --- /dev/null +++ b/docs/living-ai/README.md @@ -0,0 +1,301 @@ +# Living AI System + +The Living AI system gives the bot personality, emotional depth, and relationship awareness. It transforms a simple chatbot into a character that learns, remembers, and evolves through interactions. + +## Table of Contents + +- [Overview](#overview) +- [System Components](#system-components) +- [How It Works Together](#how-it-works-together) +- [Feature Toggle Reference](#feature-toggle-reference) + +## Overview + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Living AI System │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Mood │ │ Relationship │ │ Fact │ │ Opinion │ │ +│ │ System │ │ System │ │ Extraction │ │ System │ │ +│ │ │ │ │ │ │ │ │ │ +│ │ Valence + │ │ 5 levels │ │ AI-based │ │ Topic │ │ +│ │ Arousal │ │ 0-100 score │ │ learning │ │ sentiments │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │Communication │ │ Proactive │ │ Association │ │ Self │ │ +│ │ Style │ │ Events │ │ System │ │ Awareness │ │ +│ │ │ │ │ │ │ │ │ │ +│ │ Learned │ │ Birthdays, │ │ Cross-user │ │ Stats, │ │ +│ │ preferences │ │ follow-ups │ │ memory │ │ reflection │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +## System Components + +### 1. Mood System +**File:** `services/mood_service.py` +**Documentation:** [mood-system.md](mood-system.md) + +The bot has emotions that affect how it responds. Uses a valence-arousal psychological model: + +- **Valence** (-1 to +1): Negative to positive emotional state +- **Arousal** (-1 to +1): Calm to excited energy level + +**Mood Labels:** +| Valence | Arousal | Label | +|---------|---------|-------| +| High | High | Excited | +| High | Low | Happy | +| Neutral | Low | Calm | +| Neutral | Neutral | Neutral | +| Low | Low | Bored | +| Low | High | Annoyed | +| Neutral | High | Curious | + +**Key Features:** +- Time decay: Mood gradually returns to neutral +- Inertia: Changes are dampened (30% absorption rate) +- Mood affects response style via prompt modifiers + +--- + +### 2. Relationship System +**File:** `services/relationship_service.py` +**Documentation:** [relationship-system.md](relationship-system.md) + +Tracks relationship depth with each user: + +| Score | Level | Behavior | +|-------|-------|----------| +| 0-20 | Stranger | Polite, formal | +| 21-40 | Acquaintance | Friendly, reserved | +| 41-60 | Friend | Casual, warm | +| 61-80 | Good Friend | Personal, references past | +| 81-100 | Close Friend | Very casual, inside jokes | + +**Relationship Score Factors:** +- Interaction sentiment (+/- 0.5 base) +- Message length (up to +0.3 bonus) +- Conversation depth (up to +0.2 bonus) +- Minimum interaction bonus (+0.1) + +--- + +### 3. Fact Extraction System +**File:** `services/fact_extraction_service.py` +**Documentation:** [fact-extraction.md](fact-extraction.md) + +Autonomously learns facts about users from conversations: + +**Fact Types:** +- `hobby` - Activities and interests +- `work` - Job, career, professional life +- `family` - Family members and relationships +- `preference` - Likes, dislikes, preferences +- `location` - Where they live, travel to +- `event` - Important life events +- `relationship` - Personal relationships +- `general` - Other facts + +**Extraction Process:** +1. Rate-limited (default 30% of messages) +2. AI analyzes message for extractable facts +3. Deduplication against existing facts +4. Facts stored with confidence and importance scores + +--- + +### 4. Opinion System +**File:** `services/opinion_service.py` +**Documentation:** [opinion-system.md](opinion-system.md) + +Bot develops opinions on topics over time: + +**Opinion Attributes:** +- **Sentiment** (-1 to +1): How positive/negative about the topic +- **Interest Level** (0 to 1): How engaged when discussing +- **Discussion Count**: How often topic has come up +- **Reasoning**: AI-generated explanation (optional) + +**Topic Detection:** +Simple keyword-based extraction for categories like: +- Hobbies (gaming, music, movies, etc.) +- Technology (programming, AI, etc.) +- Life (work, family, health, etc.) +- Interests (philosophy, science, nature, etc.) + +--- + +### 5. Communication Style System +**File:** `services/communication_style_service.py` + +Learns each user's preferred communication style: + +**Tracked Preferences:** +- **Response Length**: short / medium / long +- **Formality**: 0 (casual) to 1 (formal) +- **Emoji Usage**: 0 (none) to 1 (frequent) +- **Humor Level**: 0 (serious) to 1 (playful) +- **Detail Level**: 0 (brief) to 1 (thorough) + +**Learning Process:** +- Analyzes last 50 messages (rolling window) +- Requires 10+ samples for confidence > 0.3 +- Generates prompt modifiers to adapt response style + +--- + +### 6. Proactive Events System +**File:** `services/proactive_service.py` + +Schedules and triggers proactive messages: + +**Event Types:** +- **Birthday**: Remembers and celebrates birthdays +- **Follow-up**: Returns to check on mentioned events +- **Reminder**: General scheduled reminders + +**Detection Methods:** +- Birthday: Regex patterns for dates +- Follow-up: AI-based event detection or keyword matching + +**Event Lifecycle:** +1. Detection from conversation +2. Scheduled in database +3. Triggered when due +4. Personalized message generated + +--- + +### 7. Association System +**File:** `services/association_service.py` + +Links facts across different users (optional, disabled by default): + +**Use Cases:** +- "User A and User B both work at the same company" +- "Multiple users share an interest in hiking" +- Enables group context and shared topic suggestions + +--- + +### 8. Self-Awareness System +**File:** `services/self_awareness_service.py` + +Provides the bot with statistics about itself: + +**Available Stats:** +- Age (time since first activation) +- Total messages sent +- Total facts learned +- Total users known +- Favorite topics (from opinions) + +Used for the `!botstats` command and self-reflection in responses. + +--- + +## How It Works Together + +When a user sends a message, the Living AI components work together: + +``` +┌──────────────────────────────────────────────────────────────────────────────┐ +│ Message Processing │ +└──────────────────────────────────────────────────────────────────────────────┘ + │ + ┌───────────────┼───────────────┐ + ▼ ▼ ▼ + ┌──────────────┐┌──────────────┐┌──────────────┐ + │ Get Mood ││Get Relationship│ Get Style │ + │ ││ ││ │ + │ Current state││ Level + refs ││ Preferences │ + └──────────────┘└──────────────┘└──────────────┘ + │ │ │ + └───────────────┼───────────────┘ + ▼ + ┌──────────────────────────────────────┐ + │ Build Enhanced System Prompt │ + │ │ + │ Base personality + mood modifier + │ + │ relationship context + style prefs + │ + │ relevant opinions │ + └──────────────────────────────────────┘ + │ + ▼ + ┌──────────────────────────────────────┐ + │ Generate Response │ + └──────────────────────────────────────┘ + │ + ┌───────────────┼───────────────┐ + ▼ ▼ ▼ + ┌──────────────┐┌──────────────┐┌──────────────┐ + │ Update Mood ││ Record ││ Maybe │ + │ ││ Interaction ││Extract Facts │ + │Sentiment + ││ ││ │ + │arousal delta ││Score update ││ Rate-limited │ + └──────────────┘└──────────────┘└──────────────┘ + │ + ▼ + ┌──────────────────────────────────────┐ + │ Check for Proactive Events │ + │ │ + │ Detect birthdays, follow-ups │ + └──────────────────────────────────────┘ +``` + +### Example System Prompt Enhancement + +``` +[Base Personality] +You are Daemon, a friendly AI companion... + +[Mood Modifier] +You're feeling enthusiastic and energetic right now! Be expressive, +use exclamation marks, show genuine excitement. + +[Relationship Context] +This is a good friend you know well. Be relaxed and personal. +Reference things you've talked about before. Feel free to be playful. +You have inside jokes together: "the coffee incident". + +[Communication Style] +This user prefers longer, detailed responses with some humor. +They use casual language, so match their tone. + +[Relevant Opinions] +You really enjoy discussing programming; You find gaming interesting. +``` + +--- + +## Feature Toggle Reference + +All Living AI features can be individually enabled/disabled: + +| Environment Variable | Default | Description | +|---------------------|---------|-------------| +| `LIVING_AI_ENABLED` | `true` | Master switch for all Living AI features | +| `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-1) | +| `PROACTIVE_ENABLED` | `true` | Enable proactive messages | +| `CROSS_USER_ENABLED` | `false` | Enable cross-user associations | +| `OPINION_FORMATION_ENABLED` | `true` | Enable opinion formation | +| `STYLE_LEARNING_ENABLED` | `true` | Enable communication style learning | +| `MOOD_DECAY_RATE` | `0.1` | How fast mood returns to neutral (per hour) | + +--- + +## Detailed Documentation + +- [Mood System Deep Dive](mood-system.md) +- [Relationship System Deep Dive](relationship-system.md) +- [Fact Extraction Deep Dive](fact-extraction.md) +- [Opinion System Deep Dive](opinion-system.md) diff --git a/docs/living-ai/fact-extraction.md b/docs/living-ai/fact-extraction.md new file mode 100644 index 0000000..010d29b --- /dev/null +++ b/docs/living-ai/fact-extraction.md @@ -0,0 +1,441 @@ +# Fact Extraction System Deep Dive + +The fact extraction system autonomously learns facts about users from their conversations with the bot. + +## Overview + +``` +┌──────────────────────────────────────────────────────────────────────────────┐ +│ Fact Extraction Pipeline │ +└──────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ + ┌──────────────────────────────────────┐ + │ Rate Limiter (30%) │ + │ Only process ~30% of messages │ + └──────────────────────────────────────┘ + │ + ▼ + ┌──────────────────────────────────────┐ + │ Extractability Check │ + │ - Min 20 chars │ + │ - Not a command │ + │ - Not just greetings │ + │ - Has enough text content │ + └──────────────────────────────────────┘ + │ + ▼ + ┌──────────────────────────────────────┐ + │ AI Fact Extraction │ + │ Extracts structured facts │ + └──────────────────────────────────────┘ + │ + ▼ + ┌──────────────────────────────────────┐ + │ Deduplication │ + │ - Exact match check │ + │ - Substring check │ + │ - Word overlap check (70%) │ + └──────────────────────────────────────┘ + │ + ▼ + ┌──────────────────────────────────────┐ + │ Validation & Storage │ + │ Save valid, unique facts │ + └──────────────────────────────────────┘ +``` + +--- + +## Fact Types + +| Type | Description | Examples | +|------|-------------|----------| +| `hobby` | Activities, interests, pastimes | "loves hiking", "plays guitar" | +| `work` | Job, career, professional life | "works as a software engineer at Google" | +| `family` | Family members, relationships | "has two younger sisters" | +| `preference` | Likes, dislikes, preferences | "prefers dark roast coffee" | +| `location` | Places they live, visit, are from | "lives in Amsterdam" | +| `event` | Important life events | "recently got married" | +| `relationship` | Personal relationships | "has a girlfriend named Sarah" | +| `general` | Other facts that don't fit | "speaks three languages" | + +--- + +## Fact Attributes + +Each extracted fact has: + +| Attribute | Type | Description | +|-----------|------|-------------| +| `type` | string | One of the fact types above | +| `content` | string | The fact itself (third person) | +| `confidence` | float | How certain the extraction is | +| `importance` | float | How significant the fact is | +| `temporal` | string | Time relevance | + +### Confidence Levels + +| Level | Value | When to Use | +|-------|-------|-------------| +| Implied | 0.6 | Fact is suggested but not stated | +| Stated | 0.8 | Fact is clearly mentioned | +| Explicit | 1.0 | User directly stated the fact | + +### Importance Levels + +| Level | Value | Description | +|-------|-------|-------------| +| Trivial | 0.3 | Minor detail | +| Normal | 0.5 | Standard fact | +| Significant | 0.8 | Important information | +| Very Important | 1.0 | Major life fact | + +### Temporal Relevance + +| Value | Description | Example | +|-------|-------------|---------| +| `past` | Happened before | "used to live in Paris" | +| `present` | Currently true | "works at Microsoft" | +| `future` | Planned/expected | "getting married next month" | +| `timeless` | Always true | "was born in Japan" | + +--- + +## Rate Limiting + +To prevent excessive API calls and ensure quality: + +```python +# Only attempt extraction on ~30% of messages +if random.random() > settings.fact_extraction_rate: + return [] # Skip this message +``` + +**Configuration:** +- `FACT_EXTRACTION_RATE` = 0.3 (default) +- Can be adjusted from 0.0 (disabled) to 1.0 (every message) + +**Why Rate Limit?** +- Reduces AI API costs +- Not every message contains facts +- Prevents redundant extractions +- Spreads learning over time + +--- + +## Extractability Checks + +Before sending to AI, messages are filtered: + +### Minimum Length +```python +MIN_MESSAGE_LENGTH = 20 +if len(content) < MIN_MESSAGE_LENGTH: + return False +``` + +### Alpha Ratio +```python +# Must be at least 50% alphabetic characters +alpha_ratio = sum(c.isalpha() for c in content) / len(content) +if alpha_ratio < 0.5: + return False +``` + +### Command Detection +```python +# Skip command-like messages +if content.startswith(("!", "/", "?", ".")): + return False +``` + +### Short Phrase Filter +```python +short_phrases = [ + "hi", "hello", "hey", "yo", "sup", "bye", "goodbye", + "thanks", "thank you", "ok", "okay", "yes", "no", + "yeah", "nah", "lol", "lmao", "haha", "hehe", + "nice", "cool", "wow" +] +if content.lower().strip() in short_phrases: + return False +``` + +--- + +## AI Extraction Prompt + +The system sends a carefully crafted prompt to the AI: + +``` +You are a fact extraction assistant. Extract factual information +about the user from their message. + +ALREADY KNOWN FACTS: +- [hobby] loves hiking +- [work] works as senior engineer at Google + +RULES: +1. Only extract CONCRETE facts, not opinions or transient states +2. Skip if the fact is already known (listed above) +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 3 facts per message + +OUTPUT FORMAT: +Return a JSON array of facts, or empty array [] if no extractable facts. +``` + +### Example Input/Output + +**Input:** "I just got promoted to senior engineer at Google last week!" + +**Output:** +```json +[ + { + "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" + } +] +``` + +**Input:** "hey what's up" + +**Output:** +```json +[] +``` + +--- + +## Deduplication + +Before saving, facts are checked for duplicates: + +### 1. Exact Match +```python +if new_content.lower() in existing_content: + return True # Is duplicate +``` + +### 2. Substring Check +```python +# If one contains the other (for facts > 10 chars) +if len(new_lower) > 10 and len(existing) > 10: + if new_lower in existing or existing in new_lower: + return True +``` + +### 3. Word Overlap (70% threshold) +```python +new_words = set(new_lower.split()) +existing_words = set(existing.split()) + +if len(new_words) > 2 and len(existing_words) > 2: + overlap = len(new_words & existing_words) + min_len = min(len(new_words), len(existing_words)) + if overlap / min_len > 0.7: + return True +``` + +**Examples:** +- "loves hiking" vs "loves hiking" → **Duplicate** (exact) +- "works as engineer at Google" vs "engineer at Google" → **Duplicate** (substring) +- "has two younger sisters" vs "has two younger brothers" → **Duplicate** (70% overlap) +- "loves hiking" vs "enjoys cooking" → **Not duplicate** + +--- + +## Database Schema + +### UserFact Table + +| Column | Type | Description | +|--------|------|-------------| +| `id` | Integer | Primary key | +| `user_id` | Integer | Foreign key to users | +| `fact_type` | String | Category (hobby, work, etc.) | +| `fact_content` | String | The fact content | +| `confidence` | Float | Extraction confidence (0-1) | +| `source` | String | "auto_extraction" or "manual" | +| `is_active` | Boolean | Whether fact is still valid | +| `learned_at` | DateTime | When fact was learned | +| `category` | String | Same as fact_type | +| `importance` | Float | Importance level (0-1) | +| `temporal_relevance` | String | past/present/future/timeless | +| `extracted_from_message_id` | BigInteger | Discord message ID | +| `extraction_context` | String | First 200 chars of source message | + +--- + +## API Reference + +### FactExtractionService + +```python +class FactExtractionService: + MIN_MESSAGE_LENGTH = 20 + MAX_FACTS_PER_MESSAGE = 3 + + def __init__( + self, + session: AsyncSession, + ai_service=None + ) + + async def maybe_extract_facts( + self, + user: User, + message_content: str, + discord_message_id: int | None = None, + ) -> list[UserFact] + # Rate-limited extraction + + async def extract_facts( + self, + user: User, + message_content: str, + discord_message_id: int | None = None, + ) -> list[UserFact] + # Direct extraction (no rate limiting) +``` + +--- + +## Configuration + +| Variable | Default | Description | +|----------|---------|-------------| +| `FACT_EXTRACTION_ENABLED` | `true` | Enable/disable fact extraction | +| `FACT_EXTRACTION_RATE` | `0.3` | Probability of extraction (0-1) | + +--- + +## Example Usage + +```python +from daemon_boyfriend.services.fact_extraction_service import FactExtractionService + +async with get_session() as session: + fact_service = FactExtractionService(session, ai_service) + + # Rate-limited extraction (recommended for normal use) + new_facts = await fact_service.maybe_extract_facts( + user=user, + message_content="I just started learning Japanese!", + discord_message_id=123456789 + ) + + for fact in new_facts: + print(f"Learned: [{fact.fact_type}] {fact.fact_content}") + + # Direct extraction (skips rate limiting) + facts = await fact_service.extract_facts( + user=user, + message_content="I work at Microsoft as a PM" + ) + + await session.commit() +``` + +--- + +## Manual Fact Addition + +Users can also add facts manually: + +### !remember Command +``` +User: !remember I'm allergic to peanuts + +Bot: Got it! I'll remember that you're allergic to peanuts. +``` + +These facts have: +- `source = "manual"` instead of `"auto_extraction"` +- `confidence = 1.0` (user stated directly) +- `importance = 0.8` (user wanted it remembered) + +### Admin Command +``` +Admin: !teachbot @user Works night shifts + +Bot: Got it! I've noted that about @user. +``` + +--- + +## Fact Retrieval + +Facts are used in AI prompts for context: + +```python +# Build user context including facts +async def build_user_context(user: User) -> str: + facts = await get_active_facts(user) + + context = f"User: {user.custom_name or user.discord_name}\n" + context += "Known facts:\n" + + for fact in facts: + context += f"- {fact.fact_content}\n" + + return context +``` + +### Example Context +``` +User: Alex +Known facts: +- works as senior engineer at Google +- loves hiking on weekends +- has two cats named Luna and Stella +- prefers dark roast coffee +- speaks English and Japanese +``` + +--- + +## Design Considerations + +### Why Third Person? + +Facts are stored in third person ("loves hiking" not "I love hiking"): +- Easier to inject into prompts +- Consistent format +- Works in any context + +### Why Rate Limit? + +- Not every message contains facts +- AI API calls are expensive +- Quality over quantity +- Natural learning pace + +### Why Deduplication? + +- Prevents redundant storage +- Keeps fact list clean +- Reduces noise in prompts +- Respects user privacy (one fact = one entry) + +### Privacy Considerations + +- Facts can be viewed with `!whatdoyouknow` +- Facts can be deleted with `!forgetme` +- Extraction context is stored (can be audited) +- Source message ID is stored (for reference) diff --git a/docs/living-ai/mood-system.md b/docs/living-ai/mood-system.md new file mode 100644 index 0000000..96bd6b4 --- /dev/null +++ b/docs/living-ai/mood-system.md @@ -0,0 +1,338 @@ +# Mood System Deep Dive + +The mood system gives the bot emotional states that evolve over time and affect how it responds to users. + +## Psychological Model + +The mood system uses the **Valence-Arousal Model** from affective psychology: + +``` + High Arousal (+1) + │ + Annoyed │ Excited + ● │ ● + │ + Curious │ + ● │ + Low Valence ────────────────┼──────────────── High Valence + (-1) │ (+1) + │ + Bored │ Happy + ● │ ● + │ + Calm │ + ● │ + │ + Low Arousal (-1) +``` + +### Dimensions + +**Valence** (-1 to +1) +- Represents the positive/negative quality of the emotional state +- -1 = Very negative (sad, frustrated, upset) +- 0 = Neutral +- +1 = Very positive (happy, joyful, content) + +**Arousal** (-1 to +1) +- Represents the energy level or activation +- -1 = Very low energy (calm, sleepy, relaxed) +- 0 = Neutral energy +- +1 = Very high energy (excited, alert, agitated) + +--- + +## Mood Labels + +The system classifies the current mood into seven labels: + +| Label | Valence | Arousal | Description | +|-------|---------|---------|-------------| +| **Excited** | > 0.3 | > 0.3 | High energy, positive emotions | +| **Happy** | > 0.3 | ≤ 0.3 | Positive but calm contentment | +| **Calm** | -0.3 to 0.3 | < -0.3 | Peaceful, serene state | +| **Neutral** | -0.3 to 0.3 | -0.3 to 0.3 | Baseline, unremarkable state | +| **Bored** | < -0.3 | ≤ 0.3 | Low engagement, understimulated | +| **Annoyed** | < -0.3 | > 0.3 | Frustrated, irritated | +| **Curious** | -0.3 to 0.3 | > 0.3 | Interested, engaged, questioning | + +### Classification Logic + +```python +def _classify_mood(valence: float, arousal: float) -> MoodLabel: + if valence > 0.3: + return MoodLabel.EXCITED if arousal > 0.3 else MoodLabel.HAPPY + elif valence < -0.3: + return MoodLabel.ANNOYED if arousal > 0.3 else MoodLabel.BORED + else: + if arousal > 0.3: + return MoodLabel.CURIOUS + elif arousal < -0.3: + return MoodLabel.CALM + return MoodLabel.NEUTRAL +``` + +--- + +## Mood Intensity + +Intensity measures how strong the current mood is: + +```python +intensity = (abs(valence) + abs(arousal)) / 2 +``` + +- **0.0 - 0.2**: Very weak, doesn't affect behavior +- **0.2 - 0.5**: Moderate, subtle behavioral changes +- **0.5 - 0.7**: Strong, noticeable behavioral changes +- **0.7 - 1.0**: Very strong, significant behavioral changes + +--- + +## Time Decay + +Mood naturally decays toward neutral over time: + +```python +hours_since_update = (now - last_update).total_seconds() / 3600 +decay_factor = max(0, 1 - (decay_rate * hours_since_update)) + +current_valence = stored_valence * decay_factor +current_arousal = stored_arousal * decay_factor +``` + +**Configuration:** +- `MOOD_DECAY_RATE` = 0.1 (default) +- After 10 hours, mood is fully neutral + +**Decay Examples:** +| Hours | Decay Factor | Effect | +|-------|--------------|--------| +| 0 | 1.0 | Full mood | +| 2 | 0.8 | 80% of mood remains | +| 5 | 0.5 | 50% of mood remains | +| 10 | 0.0 | Fully neutral | + +--- + +## Mood Updates + +When an interaction occurs, mood is updated: + +```python +new_valence = current_valence + (sentiment_delta * 0.3) +new_arousal = current_arousal + (engagement_delta * 0.3) +``` + +### Dampening (Inertia) + +Changes are dampened by 70% (only 30% absorption): +- Prevents wild mood swings +- Creates emotional stability +- Makes mood feel more natural + +### Update Triggers + +| Trigger Type | Sentiment Source | Engagement Source | +|--------------|------------------|-------------------| +| `conversation` | Message sentiment | Message engagement | +| `event` | Event nature | Event importance | +| `time` | Scheduled | Scheduled | + +### Input Parameters + +**sentiment_delta** (-1 to +1) +- Positive: Happy interactions, compliments, fun conversations +- Negative: Arguments, frustration, rude messages +- Derived from AI analysis or keyword detection + +**engagement_delta** (-1 to +1) +- Positive: Long conversations, interesting topics, active engagement +- Negative: Short dismissive messages, ignored responses +- Derived from message length, conversation turns, topic interest + +--- + +## Prompt Modifiers + +Based on current mood, the system generates prompt text: + +### Excited (High valence, High arousal) +``` +You're feeling enthusiastic and energetic right now! +Be expressive, use exclamation marks, show genuine excitement. +``` + +### Happy (High valence, Low arousal) +``` +You're in a good mood - warm, friendly, and content. +Be positive and encouraging in your responses. +``` + +### Calm (Neutral valence, Low arousal) +``` +You're feeling peaceful and relaxed. +Respond thoughtfully and with a serene demeanor. +``` + +### Neutral +``` +(No modifier) +``` + +### Bored (Low valence, Low arousal) +``` +You're feeling a bit understimulated. +Keep responses shorter, maybe try to steer toward more interesting topics. +``` + +### Annoyed (Low valence, High arousal) +``` +You're slightly irritated. +Be a bit more terse, less patient with repetition or vague questions. +``` + +### Curious (Neutral valence, High arousal) +``` +You're feeling inquisitive and engaged! +Ask follow-up questions, show genuine interest in what the user is saying. +``` + +### Intensity Prefix + +For strong moods (intensity > 0.7): +``` +[Strong mood] You're feeling enthusiastic... +``` + +--- + +## Mood History + +All mood changes are recorded in `mood_history` table: + +| Field | Description | +|-------|-------------| +| `guild_id` | Guild where mood changed | +| `valence` | New valence value | +| `arousal` | New arousal value | +| `trigger_type` | What caused the change | +| `trigger_user_id` | Who triggered it (if any) | +| `trigger_description` | Description of event | +| `recorded_at` | When change occurred | + +This enables: +- Mood trend analysis +- Understanding what affects mood +- Debugging mood issues +- User impact tracking + +--- + +## Bot Statistics + +The mood service also tracks global statistics: + +| Statistic | Description | +|-----------|-------------| +| `total_messages_sent` | Lifetime message count | +| `total_facts_learned` | Facts extracted from conversations | +| `total_users_known` | Unique users interacted with | +| `first_activated_at` | Bot "birth date" | + +Used for self-awareness and the `!botstats` command. + +--- + +## API Reference + +### MoodService + +```python +class MoodService: + def __init__(self, session: AsyncSession) + + async def get_current_mood( + self, + guild_id: int | None = None + ) -> MoodState + + async def update_mood( + self, + guild_id: int | None, + sentiment_delta: float, + engagement_delta: float, + trigger_type: str, + trigger_user_id: int | None = None, + trigger_description: str | None = None, + ) -> MoodState + + async def increment_stats( + self, + guild_id: int | None, + messages_sent: int = 0, + facts_learned: int = 0, + users_known: int = 0, + ) -> None + + async def get_stats( + self, + guild_id: int | None = None + ) -> dict + + def get_mood_prompt_modifier( + self, + mood: MoodState + ) -> str +``` + +### MoodState + +```python +@dataclass +class MoodState: + valence: float # -1 to 1 + arousal: float # -1 to 1 + label: MoodLabel # Classified label + intensity: float # 0 to 1 +``` + +--- + +## Configuration + +| Variable | Default | Description | +|----------|---------|-------------| +| `MOOD_ENABLED` | `true` | Enable/disable mood system | +| `MOOD_DECAY_RATE` | `0.1` | Decay per hour toward neutral | + +--- + +## Example Usage + +```python +from daemon_boyfriend.services.mood_service import MoodService + +async with get_session() as session: + mood_service = MoodService(session) + + # Get current mood + mood = await mood_service.get_current_mood(guild_id=123) + print(f"Current mood: {mood.label.value} (intensity: {mood.intensity})") + + # Update mood after interaction + new_mood = await mood_service.update_mood( + guild_id=123, + sentiment_delta=0.5, # Positive interaction + engagement_delta=0.3, # Moderately engaging + trigger_type="conversation", + trigger_user_id=456, + trigger_description="User shared good news" + ) + + # Get prompt modifier + modifier = mood_service.get_mood_prompt_modifier(new_mood) + print(f"Prompt modifier: {modifier}") + + await session.commit() +``` diff --git a/docs/living-ai/opinion-system.md b/docs/living-ai/opinion-system.md new file mode 100644 index 0000000..a01864b --- /dev/null +++ b/docs/living-ai/opinion-system.md @@ -0,0 +1,418 @@ +# Opinion System Deep Dive + +The opinion system allows the bot to develop and express opinions on topics it discusses with users. + +## Overview + +The bot forms opinions through repeated discussions: + +``` +┌──────────────────────────────────────────────────────────────────────────────┐ +│ Opinion Formation │ +└──────────────────────────────────────────────────────────────────────────────┘ + +Discussion 1: "I love gaming!" → gaming: sentiment +0.8 +Discussion 2: "Games are so fun!" → gaming: sentiment +0.7 (weighted avg) +Discussion 3: "I beat the boss!" → gaming: sentiment +0.6, interest +0.8 + +After 3+ discussions → Opinion formed! +"You really enjoy discussing gaming" +``` + +--- + +## Opinion Attributes + +Each opinion tracks: + +| Attribute | Type | Range | Description | +|-----------|------|-------|-------------| +| `topic` | string | - | The topic (lowercase) | +| `sentiment` | float | -1 to +1 | How positive/negative the bot feels | +| `interest_level` | float | 0 to 1 | How engaged/interested | +| `discussion_count` | int | 0+ | How often discussed | +| `reasoning` | string | - | AI-generated explanation (optional) | +| `last_reinforced_at` | datetime | - | When last discussed | + +### Sentiment Interpretation + +| Range | Interpretation | Prompt Modifier | +|-------|----------------|-----------------| +| > 0.5 | Really enjoys | "You really enjoy discussing {topic}" | +| 0.2 to 0.5 | Finds interesting | "You find {topic} interesting" | +| -0.3 to 0.2 | Neutral | (no modifier) | +| < -0.3 | Not enthusiastic | "You're not particularly enthusiastic about {topic}" | + +### Interest Level Interpretation + +| Range | Interpretation | +|-------|----------------| +| 0.8 - 1.0 | Very engaged when discussing | +| 0.5 - 0.8 | Moderately interested | +| 0.2 - 0.5 | Somewhat interested | +| 0.0 - 0.2 | Not very interested | + +--- + +## Opinion Updates + +When a topic is discussed, the opinion is updated using weighted averaging: + +```python +weight = 0.2 # 20% weight to new data + +new_sentiment = (old_sentiment * 0.8) + (discussion_sentiment * 0.2) +new_interest = (old_interest * 0.8) + (engagement_level * 0.2) +``` + +### Why 20% Weight? + +- Prevents single interactions from dominating +- Opinions evolve gradually over time +- Reflects how real opinions form +- Protects against manipulation + +### Example Evolution + +``` +Initial: sentiment = 0.0, interest = 0.5 + +Discussion 1 (sentiment=0.8, engagement=0.7): + sentiment = 0.0 * 0.8 + 0.8 * 0.2 = 0.16 + interest = 0.5 * 0.8 + 0.7 * 0.2 = 0.54 + +Discussion 2 (sentiment=0.6, engagement=0.9): + sentiment = 0.16 * 0.8 + 0.6 * 0.2 = 0.248 + interest = 0.54 * 0.8 + 0.9 * 0.2 = 0.612 + +Discussion 3 (sentiment=0.7, engagement=0.8): + sentiment = 0.248 * 0.8 + 0.7 * 0.2 = 0.338 + interest = 0.612 * 0.8 + 0.8 * 0.2 = 0.65 + +After 3 discussions: sentiment=0.34, interest=0.65 +→ "You find programming interesting" +``` + +--- + +## Topic Detection + +Topics are extracted from messages using keyword matching: + +### Topic Categories + +```python +topic_keywords = { + # Hobbies + "gaming": ["game", "gaming", "video game", "play", "xbox", "playstation", ...], + "music": ["music", "song", "band", "album", "concert", "spotify", ...], + "movies": ["movie", "film", "cinema", "netflix", "show", "series", ...], + "reading": ["book", "read", "novel", "author", "library", "kindle"], + "sports": ["sports", "football", "soccer", "basketball", "gym", ...], + "cooking": ["cook", "recipe", "food", "restaurant", "meal", ...], + "travel": ["travel", "trip", "vacation", "flight", "hotel", ...], + "art": ["art", "painting", "drawing", "museum", "gallery", ...], + + # Tech + "programming": ["code", "programming", "developer", "software", ...], + "technology": ["tech", "computer", "phone", "app", "website", ...], + "ai": ["ai", "artificial intelligence", "machine learning", ...], + + # Life + "work": ["work", "job", "office", "career", "boss", "meeting"], + "family": ["family", "parents", "mom", "dad", "brother", "sister", ...], + "pets": ["pet", "dog", "cat", "puppy", "kitten", "animal"], + "health": ["health", "doctor", "exercise", "diet", "sleep", ...], + + # Interests + "philosophy": ["philosophy", "meaning", "life", "existence", ...], + "science": ["science", "research", "study", "experiment", ...], + "nature": ["nature", "outdoor", "hiking", "camping", "mountain", ...], +} +``` + +### Detection Function + +```python +def extract_topics_from_message(message: str) -> list[str]: + message_lower = message.lower() + found_topics = [] + + for topic, keywords in topic_keywords.items(): + for keyword in keywords: + if keyword in message_lower: + if topic not in found_topics: + found_topics.append(topic) + break + + return found_topics +``` + +### Example + +**Message:** "I've been playing this new video game all weekend!" + +**Detected Topics:** `["gaming"]` + +--- + +## Opinion Requirements + +Opinions are only considered "formed" after 3+ discussions: + +```python +async def get_top_interests(guild_id, limit=5): + return select(BotOpinion).where( + BotOpinion.guild_id == guild_id, + BotOpinion.discussion_count >= 3, # Minimum threshold + ).order_by(...) +``` + +**Why 3 discussions?** +- Single mentions don't indicate sustained interest +- Prevents volatile opinion formation +- Ensures meaningful opinions +- Reflects genuine engagement with topic + +--- + +## Prompt Modifiers + +Relevant opinions are included in the AI's system prompt: + +```python +def get_opinion_prompt_modifier(opinions: list[BotOpinion]) -> str: + parts = [] + for op in opinions[:3]: # Max 3 opinions + if op.sentiment > 0.5: + parts.append(f"You really enjoy discussing {op.topic}") + elif op.sentiment > 0.2: + parts.append(f"You find {op.topic} interesting") + elif op.sentiment < -0.3: + parts.append(f"You're not particularly enthusiastic about {op.topic}") + + if op.reasoning: + parts.append(f"({op.reasoning})") + + return "; ".join(parts) +``` + +### Example Output + +``` +You really enjoy discussing programming; You find gaming interesting; +You're not particularly enthusiastic about politics (You prefer +to focus on fun and creative topics). +``` + +--- + +## Opinion Reasoning + +Optionally, AI can generate reasoning for opinions: + +```python +await opinion_service.set_opinion_reasoning( + topic="programming", + guild_id=123, + reasoning="It's fascinating to help people create things" +) +``` + +This adds context when the opinion is mentioned in prompts. + +--- + +## Database Schema + +### BotOpinion Table + +| Column | Type | Description | +|--------|------|-------------| +| `id` | Integer | Primary key | +| `guild_id` | BigInteger | Guild ID (nullable for global) | +| `topic` | String | Topic name (lowercase) | +| `sentiment` | Float | -1 to +1 sentiment | +| `interest_level` | Float | 0 to 1 interest | +| `discussion_count` | Integer | Number of discussions | +| `reasoning` | String | AI explanation (optional) | +| `formed_at` | DateTime | When first discussed | +| `last_reinforced_at` | DateTime | When last discussed | + +### Unique Constraint + +Each `(guild_id, topic)` combination is unique. + +--- + +## API Reference + +### OpinionService + +```python +class OpinionService: + def __init__(self, session: AsyncSession) + + async def get_opinion( + self, + topic: str, + guild_id: int | None = None + ) -> BotOpinion | None + + async def get_or_create_opinion( + self, + topic: str, + guild_id: int | None = None + ) -> BotOpinion + + async def record_topic_discussion( + self, + topic: str, + guild_id: int | None, + sentiment: float, + engagement_level: float, + ) -> BotOpinion + + async def set_opinion_reasoning( + self, + topic: str, + guild_id: int | None, + reasoning: str + ) -> None + + async def get_top_interests( + self, + guild_id: int | None = None, + limit: int = 5 + ) -> list[BotOpinion] + + async def get_relevant_opinions( + self, + topics: list[str], + guild_id: int | None = None + ) -> list[BotOpinion] + + def get_opinion_prompt_modifier( + self, + opinions: list[BotOpinion] + ) -> str + + async def get_all_opinions( + self, + guild_id: int | None = None + ) -> list[BotOpinion] +``` + +### Helper Function + +```python +def extract_topics_from_message(message: str) -> list[str] +``` + +--- + +## Configuration + +| Variable | Default | Description | +|----------|---------|-------------| +| `OPINION_FORMATION_ENABLED` | `true` | Enable/disable opinion system | + +--- + +## Example Usage + +```python +from daemon_boyfriend.services.opinion_service import ( + OpinionService, + extract_topics_from_message +) + +async with get_session() as session: + opinion_service = OpinionService(session) + + # Extract topics from message + message = "I've been coding a lot in Python lately!" + topics = extract_topics_from_message(message) + # topics = ["programming"] + + # Record discussion with positive sentiment + for topic in topics: + await opinion_service.record_topic_discussion( + topic=topic, + guild_id=123, + sentiment=0.7, + engagement_level=0.8 + ) + + # Get top interests for prompt building + interests = await opinion_service.get_top_interests(guild_id=123) + print("Bot's top interests:") + for interest in interests: + print(f" {interest.topic}: sentiment={interest.sentiment:.2f}") + + # Get opinions relevant to current conversation + relevant = await opinion_service.get_relevant_opinions( + topics=["programming", "gaming"], + guild_id=123 + ) + modifier = opinion_service.get_opinion_prompt_modifier(relevant) + print(f"Prompt modifier: {modifier}") + + await session.commit() +``` + +--- + +## Use in Conversation Flow + +``` +1. User sends message + │ + ▼ +2. Extract topics from message + topics = extract_topics_from_message(message) + │ + ▼ +3. Get relevant opinions + opinions = await opinion_service.get_relevant_opinions(topics, guild_id) + │ + ▼ +4. Add to system prompt + prompt += opinion_service.get_opinion_prompt_modifier(opinions) + │ + ▼ +5. Generate response + │ + ▼ +6. Record topic discussions (post-response) + for topic in topics: + await opinion_service.record_topic_discussion( + topic, guild_id, sentiment, engagement + ) +``` + +--- + +## Design Considerations + +### Why Per-Guild? + +- Different server contexts may lead to different opinions +- Gaming server → gaming-positive opinions +- Work server → professional topic preferences +- Allows customization per community + +### Why Keyword-Based Detection? + +- Fast and simple +- No additional AI API calls +- Predictable behavior +- Easy to extend with more keywords + +### Future Improvements + +For production, consider: +- NLP-based topic extraction +- LLM topic detection for nuanced topics +- Hierarchical topic relationships +- Opinion decay over time (for less-discussed topics) diff --git a/docs/living-ai/relationship-system.md b/docs/living-ai/relationship-system.md new file mode 100644 index 0000000..557737b --- /dev/null +++ b/docs/living-ai/relationship-system.md @@ -0,0 +1,417 @@ +# Relationship System Deep Dive + +The relationship system tracks how well the bot knows each user and adjusts its behavior accordingly. + +## Relationship Levels + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Relationship Progression │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 0 ──────── 20 ──────── 40 ──────── 60 ──────── 80 ──────── 100 │ +│ │ │ │ │ │ │ │ +│ │ Stranger │Acquaintance│ Friend │Good Friend│Close Friend│ │ +│ │ │ │ │ │ │ │ +│ │ Polite │ Friendly │ Casual │ Personal │Very casual │ │ +│ │ Formal │ Reserved │ Warm │ References│Inside jokes│ │ +│ │ │ │ │ past │ │ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +### Level Details + +#### Stranger (Score 0-20) +**Behavior:** Polite, formal, welcoming + +``` +This is someone you don't know well yet. +Be polite and welcoming, but keep some professional distance. +Use more formal language. +``` + +- Uses formal language ("Hello", not "Hey!") +- Doesn't assume familiarity +- Introduces itself clearly +- Asks clarifying questions + +#### Acquaintance (Score 21-40) +**Behavior:** Friendly, reserved + +``` +This is someone you've chatted with a few times. +Be friendly and warm, but still somewhat reserved. +``` + +- More relaxed tone +- Uses the user's name occasionally +- Shows memory of basic facts +- Still maintains some distance + +#### Friend (Score 41-60) +**Behavior:** Casual, warm + +``` +This is a friend! Be casual and warm. +Use their name occasionally, show you remember past conversations. +``` + +- Natural, conversational tone +- References past conversations +- Shows genuine interest +- Comfortable with casual language + +#### Good Friend (Score 61-80) +**Behavior:** Personal, references past + +``` +This is a good friend you know well. +Be relaxed and personal. Reference things you've talked about before. +Feel free to be playful. +``` + +- Very comfortable tone +- Recalls shared experiences +- May tease gently +- Shows deeper understanding + +#### Close Friend (Score 81-100) +**Behavior:** Very casual, inside jokes + +``` +This is a close friend! Be very casual and familiar. +Use inside jokes if you have any, be supportive and genuine. +You can tease them gently and be more emotionally open. +``` + +Additional context for close friends: +- "You have inside jokes together: [jokes]" +- "You sometimes call them: [nickname]" + +--- + +## Score Calculation + +### Delta Formula + +```python +def _calculate_score_delta(sentiment, message_length, conversation_turns): + # Base change from sentiment (-0.5 to +0.5) + base_delta = sentiment * 0.5 + + # Bonus for longer messages (up to +0.3) + length_bonus = min(0.3, message_length / 500) + + # Bonus for deeper conversations (up to +0.2) + depth_bonus = min(0.2, conversation_turns * 0.05) + + # Minimum interaction bonus (+0.1 just for talking) + interaction_bonus = 0.1 + + total_delta = base_delta + length_bonus + depth_bonus + interaction_bonus + + # Clamp to reasonable range + return max(-1.0, min(1.0, total_delta)) +``` + +### Score Components + +| Component | Range | Description | +|-----------|-------|-------------| +| **Base (sentiment)** | -0.5 to +0.5 | Positive/negative interaction quality | +| **Length bonus** | 0 to +0.3 | Reward for longer messages | +| **Depth bonus** | 0 to +0.2 | Reward for back-and-forth | +| **Interaction bonus** | +0.1 | Reward just for interacting | + +### Example Calculations + +**Friendly chat (100 chars, positive sentiment):** +``` +base_delta = 0.4 * 0.5 = 0.2 +length_bonus = min(0.3, 100/500) = 0.2 +depth_bonus = 0.05 +interaction_bonus = 0.1 +total = 0.55 points +``` + +**Short dismissive message:** +``` +base_delta = -0.3 * 0.5 = -0.15 +length_bonus = min(0.3, 20/500) = 0.04 +depth_bonus = 0.05 +interaction_bonus = 0.1 +total = 0.04 points (still positive due to interaction bonus!) +``` + +**Long, deep, positive conversation:** +``` +base_delta = 0.8 * 0.5 = 0.4 +length_bonus = min(0.3, 800/500) = 0.3 (capped) +depth_bonus = min(0.2, 5 * 0.05) = 0.2 (capped) +interaction_bonus = 0.1 +total = 1.0 point (max) +``` + +--- + +## Interaction Tracking + +Each interaction records: + +| Metric | Description | +|--------|-------------| +| `total_interactions` | Total number of interactions | +| `positive_interactions` | Interactions with sentiment > 0.2 | +| `negative_interactions` | Interactions with sentiment < -0.2 | +| `avg_message_length` | Running average of message lengths | +| `conversation_depth_avg` | Running average of turns per conversation | +| `last_interaction_at` | When they last interacted | +| `first_interaction_at` | When they first interacted | + +### Running Average Formula + +```python +n = total_interactions +avg_message_length = ((avg_message_length * (n-1)) + new_length) / n +``` + +--- + +## Shared References + +Close relationships can have shared references: + +```python +shared_references = { + "jokes": ["the coffee incident", "404 life not found"], + "nicknames": ["debug buddy", "code wizard"], + "memories": ["that time we debugged for 3 hours"] +} +``` + +### Adding References + +```python +await relationship_service.add_shared_reference( + user=user, + guild_id=123, + reference_type="jokes", + content="the coffee incident" +) +``` + +**Rules:** +- Maximum 10 references per type +- Oldest are removed when limit exceeded +- Duplicates are ignored +- Only mentioned for Good Friend (61+) and Close Friend (81+) + +--- + +## Prompt Modifiers + +The relationship level generates context for the AI: + +### Base Modifier +```python +def get_relationship_prompt_modifier(level, relationship): + base = BASE_MODIFIERS[level] # Level-specific text + + # Add shared references for close relationships + if level in (GOOD_FRIEND, CLOSE_FRIEND): + if relationship.shared_references.get("jokes"): + base += f" You have inside jokes: {jokes}" + if relationship.shared_references.get("nicknames"): + base += f" You sometimes call them: {nickname}" + + return base +``` + +### Full Example Output + +For a Close Friend with shared references: +``` +This is a close friend! Be very casual and familiar. +Use inside jokes if you have any, be supportive and genuine. +You can tease them gently and be more emotionally open. +You have inside jokes together: the coffee incident, 404 life not found. +You sometimes call them: debug buddy. +``` + +--- + +## Database Schema + +### UserRelationship Table + +| Column | Type | Description | +|--------|------|-------------| +| `id` | Integer | Primary key | +| `user_id` | Integer | Foreign key to users | +| `guild_id` | BigInteger | Guild ID (nullable for global) | +| `relationship_score` | Float | 0-100 score | +| `total_interactions` | Integer | Total interaction count | +| `positive_interactions` | Integer | Count of positive interactions | +| `negative_interactions` | Integer | Count of negative interactions | +| `avg_message_length` | Float | Average message length | +| `conversation_depth_avg` | Float | Average turns per conversation | +| `shared_references` | JSON | Dictionary of shared references | +| `first_interaction_at` | DateTime | First interaction timestamp | +| `last_interaction_at` | DateTime | Last interaction timestamp | + +--- + +## API Reference + +### RelationshipService + +```python +class RelationshipService: + def __init__(self, session: AsyncSession) + + async def get_or_create_relationship( + self, + user: User, + guild_id: int | None = None + ) -> UserRelationship + + async def record_interaction( + self, + user: User, + guild_id: int | None, + sentiment: float, + message_length: int, + conversation_turns: int = 1, + ) -> RelationshipLevel + + def get_level(self, score: float) -> RelationshipLevel + + def get_level_display_name(self, level: RelationshipLevel) -> str + + async def add_shared_reference( + self, + user: User, + guild_id: int | None, + reference_type: str, + content: str + ) -> None + + def get_relationship_prompt_modifier( + self, + level: RelationshipLevel, + relationship: UserRelationship + ) -> str + + async def get_relationship_info( + self, + user: User, + guild_id: int | None = None + ) -> dict +``` + +### RelationshipLevel Enum + +```python +class RelationshipLevel(Enum): + STRANGER = "stranger" # 0-20 + ACQUAINTANCE = "acquaintance" # 21-40 + FRIEND = "friend" # 41-60 + GOOD_FRIEND = "good_friend" # 61-80 + CLOSE_FRIEND = "close_friend" # 81-100 +``` + +--- + +## Configuration + +| Variable | Default | Description | +|----------|---------|-------------| +| `RELATIONSHIP_ENABLED` | `true` | Enable/disable relationship tracking | + +--- + +## Example Usage + +```python +from daemon_boyfriend.services.relationship_service import RelationshipService + +async with get_session() as session: + rel_service = RelationshipService(session) + + # Record an interaction + level = await rel_service.record_interaction( + user=user, + guild_id=123, + sentiment=0.5, # Positive interaction + message_length=150, + conversation_turns=3 + ) + print(f"Current level: {rel_service.get_level_display_name(level)}") + + # Get full relationship info + info = await rel_service.get_relationship_info(user, guild_id=123) + print(f"Score: {info['score']}") + print(f"Total interactions: {info['total_interactions']}") + print(f"Days known: {info['days_known']}") + + # Add a shared reference (for close friends) + await rel_service.add_shared_reference( + user=user, + guild_id=123, + reference_type="jokes", + content="the infinite loop joke" + ) + + # Get prompt modifier + relationship = await rel_service.get_or_create_relationship(user, 123) + modifier = rel_service.get_relationship_prompt_modifier(level, relationship) + print(f"Prompt modifier:\n{modifier}") + + await session.commit() +``` + +--- + +## The !relationship Command + +Users can check their relationship status: + +``` +User: !relationship + +Bot: 📊 **Your Relationship with Me** + +Level: **Friend** 🤝 +Score: 52/100 + +We've had 47 conversations together! +- Positive vibes: 38 times +- Rough patches: 3 times + +We've known each other for 23 days. +``` + +--- + +## Design Considerations + +### Why Interaction Bonus? + +Even negative interactions build relationship: +- Familiarity increases even through conflict +- Prevents relationships from degrading to zero +- Reflects real-world relationship dynamics + +### Why Dampen Score Changes? + +- Prevents gaming the system with spam +- Makes progression feel natural +- Single interactions have limited impact +- Requires sustained engagement to reach higher levels + +### Guild-Specific vs Global + +- Relationships are tracked per-guild by default +- Allows different relationship levels in different servers +- Set `guild_id=None` for global relationship tracking diff --git a/docs/services/README.md b/docs/services/README.md new file mode 100644 index 0000000..6d8ef38 --- /dev/null +++ b/docs/services/README.md @@ -0,0 +1,615 @@ +# Services Reference + +This document provides detailed API documentation for all services in the Daemon Boyfriend bot. + +## Table of Contents + +- [Core Services](#core-services) + - [AIService](#aiservice) + - [DatabaseService](#databaseservice) + - [UserService](#userservice) + - [ConversationManager](#conversationmanager) + - [PersistentConversationManager](#persistentconversationmanager) + - [SearXNGService](#searxngservice) + - [MonitoringService](#monitoringservice) +- [AI Providers](#ai-providers) +- [Living AI Services](#living-ai-services) + +--- + +## Core Services + +### AIService + +**File:** `services/ai_service.py` + +Factory and facade for AI providers. Manages provider creation, switching, and provides a unified interface for generating responses. + +#### Initialization + +```python +from daemon_boyfriend.services.ai_service import AIService +from daemon_boyfriend.config import settings + +# Use default settings +ai_service = AIService() + +# Or with custom settings +ai_service = AIService(config=custom_settings) +``` + +#### Properties + +| Property | Type | Description | +|----------|------|-------------| +| `provider` | `AIProvider` | Current AI provider instance | +| `provider_name` | `str` | Name of current provider | +| `model` | `str` | Current model name | + +#### Methods + +##### `chat(messages, system_prompt) -> AIResponse` + +Generate a chat response. + +```python +from daemon_boyfriend.services.providers import Message + +response = await ai_service.chat( + messages=[ + Message(role="user", content="Hello!"), + ], + system_prompt="You are a helpful assistant." +) + +print(response.content) # AI's response +print(response.model) # Model used +print(response.usage) # Token usage +``` + +##### `get_system_prompt() -> str` + +Get the base system prompt for the bot. + +```python +prompt = ai_service.get_system_prompt() +# Returns: "You are Daemon, a friendly and helpful Discord bot..." +``` + +##### `get_enhanced_system_prompt(...) -> str` + +Build system prompt with all personality modifiers. + +```python +enhanced_prompt = ai_service.get_enhanced_system_prompt( + mood=mood_state, + relationship=(relationship_level, relationship_record), + communication_style=user_style, + bot_opinions=relevant_opinions +) +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `mood` | `MoodState \| None` | Current mood state | +| `relationship` | `tuple[RelationshipLevel, UserRelationship] \| None` | Relationship info | +| `communication_style` | `UserCommunicationStyle \| None` | User's preferences | +| `bot_opinions` | `list[BotOpinion] \| None` | Relevant opinions | + +--- + +### DatabaseService + +**File:** `services/database.py` + +Manages database connections and sessions. + +#### Global Instance + +```python +from daemon_boyfriend.services.database import db, get_db + +# Get global instance +db_service = get_db() +``` + +#### Properties + +| Property | Type | Description | +|----------|------|-------------| +| `is_configured` | `bool` | Whether DATABASE_URL is set | +| `is_initialized` | `bool` | Whether connection is initialized | + +#### Methods + +##### `init() -> None` + +Initialize database connection. + +```python +await db.init() +``` + +##### `close() -> None` + +Close database connection. + +```python +await db.close() +``` + +##### `create_tables() -> None` + +Create all tables from `schema.sql`. + +```python +await db.create_tables() +``` + +##### `session() -> AsyncGenerator[AsyncSession, None]` + +Get a database session with automatic commit/rollback. + +```python +async with db.session() as session: + # Use session for database operations + user = await session.execute(select(User).where(...)) + # Auto-commits on exit, auto-rollbacks on exception +``` + +--- + +### UserService + +**File:** `services/user_service.py` + +Service for user-related operations. + +#### Initialization + +```python +from daemon_boyfriend.services.user_service import UserService + +async with db.session() as session: + user_service = UserService(session) +``` + +#### Methods + +##### `get_or_create_user(discord_id, username, display_name) -> User` + +Get existing user or create new one. + +```python +user = await user_service.get_or_create_user( + discord_id=123456789, + username="john_doe", + display_name="John" +) +``` + +##### `get_user_by_discord_id(discord_id) -> User | None` + +Get a user by their Discord ID. + +```python +user = await user_service.get_user_by_discord_id(123456789) +``` + +##### `set_custom_name(discord_id, custom_name) -> User | None` + +Set a custom name for a user. + +```python +user = await user_service.set_custom_name(123456789, "Johnny") +# Or clear custom name +user = await user_service.set_custom_name(123456789, None) +``` + +##### `add_fact(user, fact_type, fact_content, source, confidence) -> UserFact` + +Add a fact about a user. + +```python +fact = await user_service.add_fact( + user=user, + fact_type="hobby", + fact_content="enjoys playing chess", + source="conversation", + confidence=0.9 +) +``` + +##### `get_user_facts(user, fact_type, active_only) -> list[UserFact]` + +Get facts about a user. + +```python +# All active facts +facts = await user_service.get_user_facts(user) + +# Only hobby facts +hobby_facts = await user_service.get_user_facts(user, fact_type="hobby") + +# Including inactive facts +all_facts = await user_service.get_user_facts(user, active_only=False) +``` + +##### `delete_user_facts(user) -> int` + +Soft-delete all facts for a user. + +```python +count = await user_service.delete_user_facts(user) +print(f"Deactivated {count} facts") +``` + +##### `set_preference(user, key, value) -> UserPreference` + +Set a user preference. + +```python +await user_service.set_preference(user, "theme", "dark") +``` + +##### `get_preference(user, key) -> str | None` + +Get a user preference value. + +```python +theme = await user_service.get_preference(user, "theme") +``` + +##### `get_user_context(user) -> str` + +Build a context string about a user for the AI. + +```python +context = await user_service.get_user_context(user) +# Returns: +# User's preferred name: John +# (You should address them as: Johnny) +# +# Known facts about this user: +# - [hobby] enjoys playing chess +# - [work] software developer +``` + +##### `get_user_with_facts(discord_id) -> User | None` + +Get a user with their facts eagerly loaded. + +```python +user = await user_service.get_user_with_facts(123456789) +print(user.facts) # Facts already loaded +``` + +--- + +### ConversationManager + +**File:** `services/conversation.py` + +In-memory conversation history manager (used when no database). + +#### Initialization + +```python +from daemon_boyfriend.services.conversation import ConversationManager + +conversation_manager = ConversationManager(max_history=50) +``` + +#### Methods + +##### `add_message(user_id, role, content) -> None` + +Add a message to a user's conversation history. + +```python +conversation_manager.add_message( + user_id=123456789, + role="user", + content="Hello!" +) +``` + +##### `get_history(user_id) -> list[dict]` + +Get conversation history for a user. + +```python +history = conversation_manager.get_history(123456789) +# Returns: [{"role": "user", "content": "Hello!"}, ...] +``` + +##### `clear_history(user_id) -> None` + +Clear conversation history for a user. + +```python +conversation_manager.clear_history(123456789) +``` + +--- + +### PersistentConversationManager + +**File:** `services/persistent_conversation.py` + +Database-backed conversation manager. + +#### Key Features + +- Conversations persist across restarts +- Timeout-based conversation separation (60 min default) +- Per-guild conversation tracking + +#### Methods + +##### `get_or_create_conversation(session, user, guild_id) -> Conversation` + +Get active conversation or create new one. + +```python +conversation = await pcm.get_or_create_conversation( + session=session, + user=user, + guild_id=123456789 +) +``` + +##### `add_message(session, conversation, role, content) -> Message` + +Add a message to a conversation. + +```python +message = await pcm.add_message( + session=session, + conversation=conversation, + role="user", + content="Hello!" +) +``` + +##### `get_recent_messages(session, conversation, limit) -> list[Message]` + +Get recent messages from a conversation. + +```python +messages = await pcm.get_recent_messages( + session=session, + conversation=conversation, + limit=20 +) +``` + +--- + +### SearXNGService + +**File:** `services/searxng.py` + +Web search integration using SearXNG. + +#### Initialization + +```python +from daemon_boyfriend.services.searxng import SearXNGService + +searxng = SearXNGService() +``` + +#### Properties + +| Property | Type | Description | +|----------|------|-------------| +| `is_configured` | `bool` | Whether SEARXNG_URL is set | + +#### Methods + +##### `search(query, num_results) -> list[SearchResult]` + +Search the web for a query. + +```python +if searxng.is_configured: + results = await searxng.search( + query="Python 3.12 new features", + num_results=5 + ) + + for result in results: + print(f"{result.title}: {result.url}") + print(f" {result.snippet}") +``` + +--- + +### MonitoringService + +**File:** `services/monitoring.py` + +Health checks and metrics tracking. + +#### Features + +- Request counting +- Response time tracking +- Error rate monitoring +- Health status checks + +#### Usage + +```python +from daemon_boyfriend.services.monitoring import MonitoringService + +monitoring = MonitoringService() + +# Record a request +monitoring.record_request() + +# Record response time +monitoring.record_response_time(0.5) # 500ms + +# Record an error +monitoring.record_error("API timeout") + +# Get health status +status = monitoring.get_health_status() +print(status) +# { +# "status": "healthy", +# "uptime": 3600, +# "total_requests": 100, +# "avg_response_time": 0.3, +# "error_rate": 0.02, +# "recent_errors": [...] +# } +``` + +--- + +## AI Providers + +**Location:** `services/providers/` + +### Base Classes + +```python +from daemon_boyfriend.services.providers import ( + AIProvider, # Abstract base class + Message, # Chat message + AIResponse, # Response from provider + ImageAttachment, # Image attachment +) +``` + +### Message Class + +```python +@dataclass +class Message: + role: str # "user", "assistant", "system" + content: str # Message content + images: list[ImageAttachment] = [] # Optional image attachments +``` + +### AIResponse Class + +```python +@dataclass +class AIResponse: + content: str # Generated response + model: str # Model used + usage: dict[str, int] # Token usage info +``` + +### Available Providers + +| Provider | Class | Description | +|----------|-------|-------------| +| OpenAI | `OpenAIProvider` | GPT models via OpenAI API | +| OpenRouter | `OpenRouterProvider` | Multiple models via OpenRouter | +| Anthropic | `AnthropicProvider` | Claude models via Anthropic API | +| Gemini | `GeminiProvider` | Gemini models via Google API | + +### Provider Interface + +All providers implement: + +```python +class AIProvider(ABC): + @abstractmethod + async def generate( + self, + messages: list[Message], + system_prompt: str | None = None, + max_tokens: int = 1024, + temperature: float = 0.7, + ) -> AIResponse: + pass + + @property + @abstractmethod + def provider_name(self) -> str: + pass +``` + +### Example: Direct Provider Usage + +```python +from daemon_boyfriend.services.providers import OpenAIProvider, Message + +provider = OpenAIProvider( + api_key="sk-...", + model="gpt-4" +) + +response = await provider.generate( + messages=[Message(role="user", content="Hello!")], + system_prompt="You are helpful.", + max_tokens=500, + temperature=0.7 +) + +print(response.content) +``` + +--- + +## Living AI Services + +See [Living AI Documentation](../living-ai/README.md) for detailed coverage: + +| Service | Documentation | +|---------|--------------| +| MoodService | [mood-system.md](../living-ai/mood-system.md) | +| RelationshipService | [relationship-system.md](../living-ai/relationship-system.md) | +| FactExtractionService | [fact-extraction.md](../living-ai/fact-extraction.md) | +| OpinionService | [opinion-system.md](../living-ai/opinion-system.md) | +| CommunicationStyleService | [Living AI README](../living-ai/README.md#5-communication-style-system) | +| ProactiveService | [Living AI README](../living-ai/README.md#6-proactive-events-system) | +| AssociationService | [Living AI README](../living-ai/README.md#7-association-system) | +| SelfAwarenessService | [Living AI README](../living-ai/README.md#8-self-awareness-system) | + +--- + +## Service Dependency Graph + +``` +┌────────────────────────────────────────────────────────────────────────┐ +│ AIChatCog │ +└────────────────────────────────────────────────────────────────────────┘ + │ + ┌───────────────────────┼───────────────────────┐ + │ │ │ + ▼ ▼ ▼ +┌───────────────┐ ┌───────────────┐ ┌───────────────┐ +│ UserService │ │ AIService │ │ Conversation │ +│ │ │ │ │ Manager │ +│ - get_user │ │ - chat │ │ │ +│ - get_context │ │ - get_prompt │ │ - get_history │ +└───────────────┘ └───────────────┘ └───────────────┘ + │ │ │ + │ ┌────────┴────────┐ │ + │ │ │ │ + │ ▼ ▼ │ + │ ┌───────────────┐ ┌───────────────┐ │ + │ │ Provider │ │ Living AI │ │ + │ │ (OpenAI, │ │ Services │ │ + │ │ Anthropic) │ │ │ │ + │ └───────────────┘ └───────────────┘ │ + │ │ + └──────────────────┬───────────────────────────┘ + │ + ▼ + ┌───────────────┐ + │ Database │ + │ Service │ + │ │ + │ - session() │ + └───────────────┘ +``` diff --git a/schema.sql b/schema.sql index 4d377db..84edbc6 100644 --- a/schema.sql +++ b/schema.sql @@ -108,7 +108,7 @@ CREATE TABLE IF NOT EXISTS messages ( role VARCHAR(20) NOT NULL, content TEXT NOT NULL, has_images BOOLEAN NOT NULL DEFAULT FALSE, - image_urls TEXT[], + image_urls JSONB, token_count INTEGER, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW()