quick fix

This commit is contained in:
2026-01-12 20:30:59 +01:00
parent bf01724b3e
commit 743bed67f3
16 changed files with 146 additions and 102 deletions

View File

@@ -29,16 +29,16 @@ AI_TEMPERATURE=0.7
# Bot Identity & Personality
# ===========================================
# The bot's name, used in the system prompt to tell the AI who it is
BOT_NAME=My Bot
BOT_NAME="My Bot"
# Personality traits that define how the bot responds (used in system prompt)
BOT_PERSONALITY=helpful and friendly
BOT_PERSONALITY="helpful and friendly"
# Message shown when someone mentions the bot without saying anything
BOT_DESCRIPTION=I'm an AI assistant here to help you.
BOT_DESCRIPTION="I'm an AI assistant here to help you."
# Status message shown in Discord (displays as "Watching <BOT_STATUS>")
BOT_STATUS=for mentions
BOT_STATUS="for mentions"
# Optional: Override the entire system prompt (leave commented to use auto-generated)
# SYSTEM_PROMPT=You are a custom assistant...
@@ -104,7 +104,7 @@ FACT_EXTRACTION_RATE=0.3
PROACTIVE_ENABLED=true
# Enable cross-user associations (privacy-sensitive - shows shared interests)
CROSS_USER_ENABLED=false
CROSS_USER_ENABLED=true
# Enable bot opinion formation (bot develops topic preferences)
OPINION_FORMATION_ENABLED=true
@@ -119,18 +119,18 @@ MOOD_DECAY_RATE=0.1
# Command Toggles
# ===========================================
# Master switch for all commands (when false, bot handles via conversation)
COMMANDS_ENABLED=true
COMMANDS_ENABLED=false
# Individual command toggles
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
# 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 & Monitoring

View File

@@ -1,32 +1,33 @@
services:
daemon-boyfriend:
build: .
container_name: daemon-boyfriend
restart: unless-stopped
env_file:
- .env
environment:
- PYTHONUNBUFFERED=1
- DATABASE_URL=postgresql+asyncpg://daemon:${POSTGRES_PASSWORD:-daemon}@postgres:5432/daemon_boyfriend
depends_on:
postgres:
condition: service_healthy
daemon-boyfriend:
build: .
container_name: daemon-boyfriend
restart: unless-stopped
env_file:
- .env
environment:
- PYTHONUNBUFFERED=1
- DATABASE_URL=postgresql+asyncpg://daemon:${POSTGRES_PASSWORD:-daemon}@postgres:5432/daemon_boyfriend
depends_on:
postgres:
condition: service_healthy
postgres:
image: postgres:16-alpine
container_name: daemon-boyfriend-postgres
restart: unless-stopped
environment:
POSTGRES_USER: daemon
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-daemon}
POSTGRES_DB: daemon_boyfriend
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U daemon -d daemon_boyfriend"]
interval: 10s
timeout: 5s
retries: 5
postgres:
image: postgres:16-alpine
container_name: daemon-boyfriend-postgres
restart: unless-stopped
environment:
POSTGRES_USER: daemon
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-daemon}
POSTGRES_DB: daemon_boyfriend
volumes:
- postgres_data:/var/lib/postgresql/data
- ./schema.sql:/docker-entrypoint-initdb.d/schema.sql:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U daemon -d daemon_boyfriend"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres_data:
postgres_data:

View File

@@ -1,11 +1,17 @@
"""SQLAlchemy base model and metadata configuration."""
from datetime import datetime
from datetime import datetime, timezone
from sqlalchemy import MetaData
from sqlalchemy import DateTime, MetaData
from sqlalchemy.ext.asyncio import AsyncAttrs
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
def utc_now() -> datetime:
"""Return current UTC time as timezone-aware datetime."""
return datetime.now(timezone.utc)
# Naming convention for constraints (helps with migrations)
convention = {
"ix": "ix_%(column_0_label)s",
@@ -23,6 +29,8 @@ class Base(AsyncAttrs, DeclarativeBase):
metadata = metadata
# Common timestamp columns
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(default=datetime.utcnow, onupdate=datetime.utcnow)
# Common timestamp columns - use timezone-aware datetimes
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=utc_now, onupdate=utc_now
)

View File

@@ -3,10 +3,10 @@
from datetime import datetime
from typing import TYPE_CHECKING
from sqlalchemy import ARRAY, BigInteger, Boolean, ForeignKey, Integer, String, Text
from sqlalchemy import ARRAY, BigInteger, Boolean, DateTime, ForeignKey, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from .base import Base
from .base import Base, utc_now
if TYPE_CHECKING:
from .user import User
@@ -21,8 +21,10 @@ class Conversation(Base):
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True)
guild_id: Mapped[int | None] = mapped_column(BigInteger)
channel_id: Mapped[int | None] = mapped_column(BigInteger, index=True)
started_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
last_message_at: Mapped[datetime] = mapped_column(default=datetime.utcnow, index=True)
started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
last_message_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=utc_now, index=True
)
message_count: Mapped[int] = mapped_column(Integer, default=0)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, index=True)

View File

@@ -3,11 +3,20 @@
from datetime import datetime
from typing import TYPE_CHECKING
from sqlalchemy import ARRAY, BigInteger, Boolean, ForeignKey, String, Text, UniqueConstraint
from sqlalchemy import (
ARRAY,
BigInteger,
Boolean,
DateTime,
ForeignKey,
String,
Text,
UniqueConstraint,
)
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship
from .base import Base
from .base import Base, utc_now
if TYPE_CHECKING:
from .user import User
@@ -21,7 +30,7 @@ class Guild(Base):
id: Mapped[int] = mapped_column(primary_key=True)
discord_id: Mapped[int] = mapped_column(BigInteger, unique=True, index=True)
name: Mapped[str] = mapped_column(String(255))
joined_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
joined_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
settings: Mapped[dict] = mapped_column(JSONB, default=dict)
@@ -41,7 +50,7 @@ class GuildMember(Base):
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True)
guild_nickname: Mapped[str | None] = mapped_column(String(255))
roles: Mapped[list[str] | None] = mapped_column(ARRAY(Text), default=None)
joined_guild_at: Mapped[datetime | None] = mapped_column(default=None)
joined_guild_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), default=None)
# Relationships
guild: Mapped["Guild"] = relationship(back_populates="members")

View File

@@ -3,11 +3,20 @@
from datetime import datetime
from typing import TYPE_CHECKING
from sqlalchemy import BigInteger, Boolean, Float, ForeignKey, String, Text, UniqueConstraint
from sqlalchemy import (
BigInteger,
Boolean,
DateTime,
Float,
ForeignKey,
String,
Text,
UniqueConstraint,
)
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship
from .base import Base
from .base import Base, utc_now
if TYPE_CHECKING:
from .user import User, UserFact
@@ -24,13 +33,13 @@ class BotState(Base):
# Current mood state (valence-arousal model)
mood_valence: Mapped[float] = mapped_column(Float, default=0.0) # -1.0 (sad) to 1.0 (happy)
mood_arousal: Mapped[float] = mapped_column(Float, default=0.0) # -1.0 (calm) to 1.0 (excited)
mood_updated_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
mood_updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
# Bot statistics
total_messages_sent: Mapped[int] = mapped_column(default=0)
total_facts_learned: Mapped[int] = mapped_column(default=0)
total_users_known: Mapped[int] = mapped_column(default=0)
first_activated_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
first_activated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
# Bot preferences (evolved over time)
preferences: Mapped[dict] = mapped_column(JSONB, default=dict)
@@ -50,8 +59,8 @@ class BotOpinion(Base):
discussion_count: Mapped[int] = mapped_column(default=0)
reasoning: Mapped[str | None] = mapped_column(Text)
formed_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
last_reinforced_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
formed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
last_reinforced_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
__table_args__ = (UniqueConstraint("guild_id", "topic"),)
@@ -81,8 +90,8 @@ class UserRelationship(Base):
# Inside jokes / shared references
shared_references: Mapped[dict] = mapped_column(JSONB, default=dict)
first_interaction_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
last_interaction_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
first_interaction_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
last_interaction_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
# Relationships
user: Mapped["User"] = relationship(back_populates="relationships")
@@ -128,7 +137,7 @@ class ScheduledEvent(Base):
channel_id: Mapped[int | None] = mapped_column(BigInteger)
event_type: Mapped[str] = mapped_column(String(50), index=True)
trigger_at: Mapped[datetime] = mapped_column(index=True)
trigger_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True)
title: Mapped[str] = mapped_column(String(255))
context: Mapped[dict] = mapped_column(JSONB, default=dict)
@@ -137,7 +146,7 @@ class ScheduledEvent(Base):
recurrence_rule: Mapped[str | None] = mapped_column(String(100))
status: Mapped[str] = mapped_column(String(20), default="pending", index=True)
triggered_at: Mapped[datetime | None] = mapped_column(default=None)
triggered_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), default=None)
# Relationships
user: Mapped["User"] = relationship(back_populates="scheduled_events")
@@ -159,7 +168,7 @@ class FactAssociation(Base):
association_type: Mapped[str] = mapped_column(String(50))
strength: Mapped[float] = mapped_column(Float, default=0.5)
discovered_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
discovered_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
# Relationships
fact_1: Mapped["UserFact"] = relationship(foreign_keys=[fact_id_1])
@@ -183,4 +192,6 @@ class MoodHistory(Base):
trigger_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"))
trigger_description: Mapped[str | None] = mapped_column(Text)
recorded_at: Mapped[datetime] = mapped_column(default=datetime.utcnow, index=True)
recorded_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=utc_now, index=True
)

View File

@@ -3,10 +3,19 @@
from datetime import datetime
from typing import TYPE_CHECKING
from sqlalchemy import BigInteger, Boolean, Float, ForeignKey, String, Text, UniqueConstraint
from sqlalchemy import (
BigInteger,
Boolean,
DateTime,
Float,
ForeignKey,
String,
Text,
UniqueConstraint,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from .base import Base
from .base import Base, utc_now
if TYPE_CHECKING:
from .conversation import Conversation, Message
@@ -24,8 +33,8 @@ class User(Base):
discord_username: Mapped[str] = mapped_column(String(255))
discord_display_name: Mapped[str | None] = mapped_column(String(255))
custom_name: Mapped[str | None] = mapped_column(String(255))
first_seen_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
last_seen_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
first_seen_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
last_seen_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
# Relationships
@@ -88,8 +97,10 @@ class UserFact(Base):
confidence: Mapped[float] = mapped_column(Float, default=1.0)
source: Mapped[str] = mapped_column(String(50), default="conversation")
is_active: Mapped[bool] = mapped_column(Boolean, default=True, index=True)
learned_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
last_referenced_at: Mapped[datetime | None] = mapped_column(default=None)
learned_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
last_referenced_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), default=None
)
# Relationships
user: Mapped["User"] = relationship(back_populates="facts")

View File

@@ -1,7 +1,7 @@
"""Association Service - discovers and manages cross-user fact associations."""
import logging
from datetime import datetime
from datetime import datetime, timezone
from sqlalchemy import and_, select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -97,7 +97,7 @@ class AssociationService:
fact_id_2=fact_2.id,
association_type=association_type,
strength=strength,
discovered_at=datetime.utcnow(),
discovered_at=datetime.now(timezone.utc),
)
self._session.add(assoc)
await self._session.flush()

View File

@@ -3,7 +3,7 @@
import json
import logging
import random
from datetime import datetime
from datetime import datetime, timezone
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -311,7 +311,7 @@ Return ONLY the JSON array, no other text."""
confidence=fact_data.get("confidence", 0.8),
source="auto_extraction",
is_active=True,
learned_at=datetime.utcnow(),
learned_at=datetime.now(timezone.utc),
# New fields from Living AI
category=fact_data["type"],
importance=fact_data.get("importance", 0.5),

View File

@@ -2,7 +2,7 @@
import logging
from dataclasses import dataclass
from datetime import datetime
from datetime import datetime, timezone
from enum import Enum
from sqlalchemy import select
@@ -60,7 +60,9 @@ class MoodService:
bot_state = await self.get_or_create_bot_state(guild_id)
# Apply time decay toward neutral
hours_since_update = (datetime.utcnow() - bot_state.mood_updated_at).total_seconds() / 3600
hours_since_update = (
datetime.now(timezone.utc) - bot_state.mood_updated_at
).total_seconds() / 3600
decay_factor = max(0, 1 - (settings.mood_decay_rate * hours_since_update))
valence = bot_state.mood_valence * decay_factor
@@ -105,7 +107,7 @@ class MoodService:
bot_state = await self.get_or_create_bot_state(guild_id)
bot_state.mood_valence = new_valence
bot_state.mood_arousal = new_arousal
bot_state.mood_updated_at = datetime.utcnow()
bot_state.mood_updated_at = datetime.now(timezone.utc)
# Record history
await self._record_mood_history(
@@ -140,7 +142,7 @@ class MoodService:
async def get_stats(self, guild_id: int | None = None) -> dict:
"""Get bot statistics."""
bot_state = await self.get_or_create_bot_state(guild_id)
age_delta = datetime.utcnow() - bot_state.first_activated_at
age_delta = datetime.now(timezone.utc) - bot_state.first_activated_at
return {
"age_days": age_delta.days,

View File

@@ -1,7 +1,7 @@
"""Opinion Service - manages bot opinion formation on topics."""
import logging
from datetime import datetime
from datetime import datetime, timezone
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -77,7 +77,7 @@ class OpinionService:
)
opinion.interest_level = max(0.0, min(1.0, opinion.interest_level))
opinion.last_reinforced_at = datetime.utcnow()
opinion.last_reinforced_at = datetime.now(timezone.utc)
logger.debug(
f"Updated opinion on '{topic}': sentiment={opinion.sentiment:.2f}, "

View File

@@ -1,7 +1,7 @@
"""Persistent conversation management using PostgreSQL."""
import logging
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -38,7 +38,7 @@ class PersistentConversationManager:
Conversation model instance
"""
# Look for recent active conversation in this channel
cutoff = datetime.utcnow() - self._timeout
cutoff = datetime.now(timezone.utc) - self._timeout
stmt = select(Conversation).where(
Conversation.user_id == user.id,
@@ -133,7 +133,7 @@ class PersistentConversationManager:
self._session.add(message)
# Update conversation stats
conversation.last_message_at = datetime.utcnow()
conversation.last_message_at = datetime.now(timezone.utc)
conversation.message_count += 1
await self._session.flush()

View File

@@ -3,7 +3,7 @@
import json
import logging
import re
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -71,7 +71,7 @@ Examples:
if result and result.get("has_event"):
days_until = result.get("days_until", 1) or 1
# Schedule follow-up for 1 day after the event
trigger_at = datetime.utcnow() + timedelta(days=days_until + 1)
trigger_at = datetime.now(timezone.utc) + timedelta(days=days_until + 1)
event = ScheduledEvent(
user_id=user.id,
@@ -159,7 +159,7 @@ Examples:
break
# Create the event
trigger_at = datetime.utcnow() + timedelta(days=days_until + 1)
trigger_at = datetime.now(timezone.utc) + timedelta(days=days_until + 1)
event = ScheduledEvent(
user_id=user.id,
@@ -300,7 +300,7 @@ Examples:
def _next_birthday(self, birthday: datetime) -> datetime:
"""Calculate the next occurrence of a birthday."""
today = datetime.utcnow().date()
today = datetime.now(timezone.utc).date()
this_year = birthday.replace(year=today.year)
if this_year.date() < today:
@@ -322,7 +322,7 @@ Examples:
async def get_pending_events(self, before: datetime | None = None) -> list[ScheduledEvent]:
"""Get events that should be triggered."""
cutoff = before or datetime.utcnow()
cutoff = before or datetime.now(timezone.utc)
stmt = (
select(ScheduledEvent)
.where(
@@ -390,7 +390,7 @@ Examples:
async def mark_event_triggered(self, event: ScheduledEvent) -> None:
"""Mark an event as triggered and handle recurrence."""
event.status = "triggered"
event.triggered_at = datetime.utcnow()
event.triggered_at = datetime.now(timezone.utc)
# Handle recurring events
if event.is_recurring and event.recurrence_rule:

View File

@@ -1,7 +1,7 @@
"""Relationship Service - manages relationship tracking with users."""
import logging
from datetime import datetime
from datetime import datetime, timezone
from enum import Enum
from sqlalchemy import select
@@ -69,7 +69,7 @@ class RelationshipService:
rel = await self.get_or_create_relationship(user, guild_id)
rel.total_interactions += 1
rel.last_interaction_at = datetime.utcnow()
rel.last_interaction_at = datetime.now(timezone.utc)
# Track sentiment
if sentiment > 0.2:
@@ -211,7 +211,7 @@ class RelationshipService:
level = self.get_level(rel.relationship_score)
# Calculate time since first interaction
time_known = datetime.utcnow() - rel.first_interaction_at
time_known = datetime.now(timezone.utc) - rel.first_interaction_at
days_known = time_known.days
return {

View File

@@ -1,7 +1,7 @@
"""Self Awareness Service - provides bot self-reflection and statistics."""
import logging
from datetime import datetime
from datetime import datetime, timezone
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -36,7 +36,7 @@ class SelfAwarenessService:
await self._session.flush()
# Calculate age
age_delta = datetime.utcnow() - bot_state.first_activated_at
age_delta = datetime.now(timezone.utc) - bot_state.first_activated_at
# Count users (from database)
user_count = await self._count_users()
@@ -76,7 +76,7 @@ class SelfAwarenessService:
facts_count = facts_result.scalar() or 0
if rel:
days_known = (datetime.utcnow() - rel.first_interaction_at).days
days_known = (datetime.now(timezone.utc) - rel.first_interaction_at).days
return {
"first_met": rel.first_interaction_at,
"days_known": days_known,

View File

@@ -1,7 +1,7 @@
"""User management service."""
import logging
from datetime import datetime
from datetime import datetime, timezone
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -40,7 +40,7 @@ class UserService:
if user:
# Update last seen and current name
user.last_seen_at = datetime.utcnow()
user.last_seen_at = datetime.now(timezone.utc)
user.discord_username = username
if display_name:
user.discord_display_name = display_name
@@ -232,7 +232,7 @@ class UserService:
for fact in facts[:20]: # Limit to 20 most recent facts
lines.append(f"- [{fact.fact_type}] {fact.fact_content}")
# Mark as referenced
fact.last_referenced_at = datetime.utcnow()
fact.last_referenced_at = datetime.now(timezone.utc)
return "\n".join(lines)