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 # Bot Identity & Personality
# =========================================== # ===========================================
# The bot's name, used in the system prompt to tell the AI who it is # 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) # 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 # 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>") # 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) # Optional: Override the entire system prompt (leave commented to use auto-generated)
# SYSTEM_PROMPT=You are a custom assistant... # SYSTEM_PROMPT=You are a custom assistant...
@@ -104,7 +104,7 @@ FACT_EXTRACTION_RATE=0.3
PROACTIVE_ENABLED=true PROACTIVE_ENABLED=true
# Enable cross-user associations (privacy-sensitive - shows shared interests) # 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) # Enable bot opinion formation (bot develops topic preferences)
OPINION_FORMATION_ENABLED=true OPINION_FORMATION_ENABLED=true
@@ -119,18 +119,18 @@ MOOD_DECAY_RATE=0.1
# Command Toggles # Command Toggles
# =========================================== # ===========================================
# Master switch for all commands (when false, bot handles via conversation) # Master switch for all commands (when false, bot handles via conversation)
COMMANDS_ENABLED=true COMMANDS_ENABLED=false
# Individual command toggles # Individual command toggles
CMD_RELATIONSHIP_ENABLED=true # CMD_RELATIONSHIP_ENABLED=true
CMD_MOOD_ENABLED=true # CMD_MOOD_ENABLED=true
CMD_BOTSTATS_ENABLED=true # CMD_BOTSTATS_ENABLED=true
CMD_OURHISTORY_ENABLED=true # CMD_OURHISTORY_ENABLED=true
CMD_BIRTHDAY_ENABLED=true # CMD_BIRTHDAY_ENABLED=true
CMD_REMEMBER_ENABLED=true # CMD_REMEMBER_ENABLED=true
CMD_SETNAME_ENABLED=true # CMD_SETNAME_ENABLED=true
CMD_WHATDOYOUKNOW_ENABLED=true # CMD_WHATDOYOUKNOW_ENABLED=true
CMD_FORGETME_ENABLED=true # CMD_FORGETME_ENABLED=true
# =========================================== # ===========================================
# Logging & Monitoring # Logging & Monitoring

View File

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

View File

@@ -1,11 +1,17 @@
"""SQLAlchemy base model and metadata configuration.""" """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.ext.asyncio import AsyncAttrs
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column 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) # Naming convention for constraints (helps with migrations)
convention = { convention = {
"ix": "ix_%(column_0_label)s", "ix": "ix_%(column_0_label)s",
@@ -23,6 +29,8 @@ class Base(AsyncAttrs, DeclarativeBase):
metadata = metadata metadata = metadata
# Common timestamp columns # Common timestamp columns - use timezone-aware datetimes
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
updated_at: Mapped[datetime] = mapped_column(default=datetime.utcnow, onupdate=datetime.utcnow) 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 datetime import datetime
from typing import TYPE_CHECKING 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 sqlalchemy.orm import Mapped, mapped_column, relationship
from .base import Base from .base import Base, utc_now
if TYPE_CHECKING: if TYPE_CHECKING:
from .user import User from .user import User
@@ -21,8 +21,10 @@ class Conversation(Base):
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True) user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True)
guild_id: Mapped[int | None] = mapped_column(BigInteger) guild_id: Mapped[int | None] = mapped_column(BigInteger)
channel_id: Mapped[int | None] = mapped_column(BigInteger, index=True) channel_id: Mapped[int | None] = mapped_column(BigInteger, index=True)
started_at: Mapped[datetime] = mapped_column(default=datetime.utcnow) started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
last_message_at: Mapped[datetime] = mapped_column(default=datetime.utcnow, index=True) last_message_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=utc_now, index=True
)
message_count: Mapped[int] = mapped_column(Integer, default=0) message_count: Mapped[int] = mapped_column(Integer, default=0)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, index=True) is_active: Mapped[bool] = mapped_column(Boolean, default=True, index=True)

View File

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

View File

@@ -3,11 +3,20 @@
from datetime import datetime from datetime import datetime
from typing import TYPE_CHECKING 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.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from .base import Base from .base import Base, utc_now
if TYPE_CHECKING: if TYPE_CHECKING:
from .user import User, UserFact from .user import User, UserFact
@@ -24,13 +33,13 @@ class BotState(Base):
# Current mood state (valence-arousal model) # Current mood state (valence-arousal model)
mood_valence: Mapped[float] = mapped_column(Float, default=0.0) # -1.0 (sad) to 1.0 (happy) 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_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 # Bot statistics
total_messages_sent: Mapped[int] = mapped_column(default=0) total_messages_sent: Mapped[int] = mapped_column(default=0)
total_facts_learned: Mapped[int] = mapped_column(default=0) total_facts_learned: Mapped[int] = mapped_column(default=0)
total_users_known: 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) # Bot preferences (evolved over time)
preferences: Mapped[dict] = mapped_column(JSONB, default=dict) preferences: Mapped[dict] = mapped_column(JSONB, default=dict)
@@ -50,8 +59,8 @@ class BotOpinion(Base):
discussion_count: Mapped[int] = mapped_column(default=0) discussion_count: Mapped[int] = mapped_column(default=0)
reasoning: Mapped[str | None] = mapped_column(Text) reasoning: Mapped[str | None] = mapped_column(Text)
formed_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(default=datetime.utcnow) last_reinforced_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
__table_args__ = (UniqueConstraint("guild_id", "topic"),) __table_args__ = (UniqueConstraint("guild_id", "topic"),)
@@ -81,8 +90,8 @@ class UserRelationship(Base):
# Inside jokes / shared references # Inside jokes / shared references
shared_references: Mapped[dict] = mapped_column(JSONB, default=dict) shared_references: Mapped[dict] = mapped_column(JSONB, default=dict)
first_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(default=datetime.utcnow) last_interaction_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
# Relationships # Relationships
user: Mapped["User"] = relationship(back_populates="relationships") user: Mapped["User"] = relationship(back_populates="relationships")
@@ -128,7 +137,7 @@ class ScheduledEvent(Base):
channel_id: Mapped[int | None] = mapped_column(BigInteger) channel_id: Mapped[int | None] = mapped_column(BigInteger)
event_type: Mapped[str] = mapped_column(String(50), index=True) 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)) title: Mapped[str] = mapped_column(String(255))
context: Mapped[dict] = mapped_column(JSONB, default=dict) context: Mapped[dict] = mapped_column(JSONB, default=dict)
@@ -137,7 +146,7 @@ class ScheduledEvent(Base):
recurrence_rule: Mapped[str | None] = mapped_column(String(100)) recurrence_rule: Mapped[str | None] = mapped_column(String(100))
status: Mapped[str] = mapped_column(String(20), default="pending", index=True) 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 # Relationships
user: Mapped["User"] = relationship(back_populates="scheduled_events") user: Mapped["User"] = relationship(back_populates="scheduled_events")
@@ -159,7 +168,7 @@ class FactAssociation(Base):
association_type: Mapped[str] = mapped_column(String(50)) association_type: Mapped[str] = mapped_column(String(50))
strength: Mapped[float] = mapped_column(Float, default=0.5) 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 # Relationships
fact_1: Mapped["UserFact"] = relationship(foreign_keys=[fact_id_1]) 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_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"))
trigger_description: Mapped[str | None] = mapped_column(Text) 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 datetime import datetime
from typing import TYPE_CHECKING 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 sqlalchemy.orm import Mapped, mapped_column, relationship
from .base import Base from .base import Base, utc_now
if TYPE_CHECKING: if TYPE_CHECKING:
from .conversation import Conversation, Message from .conversation import Conversation, Message
@@ -24,8 +33,8 @@ class User(Base):
discord_username: Mapped[str] = mapped_column(String(255)) discord_username: Mapped[str] = mapped_column(String(255))
discord_display_name: Mapped[str | None] = mapped_column(String(255)) discord_display_name: Mapped[str | None] = mapped_column(String(255))
custom_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) first_seen_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
last_seen_at: Mapped[datetime] = mapped_column(default=datetime.utcnow) last_seen_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
is_active: Mapped[bool] = mapped_column(Boolean, default=True) is_active: Mapped[bool] = mapped_column(Boolean, default=True)
# Relationships # Relationships
@@ -88,8 +97,10 @@ class UserFact(Base):
confidence: Mapped[float] = mapped_column(Float, default=1.0) confidence: Mapped[float] = mapped_column(Float, default=1.0)
source: Mapped[str] = mapped_column(String(50), default="conversation") source: Mapped[str] = mapped_column(String(50), default="conversation")
is_active: Mapped[bool] = mapped_column(Boolean, default=True, index=True) is_active: Mapped[bool] = mapped_column(Boolean, default=True, index=True)
learned_at: Mapped[datetime] = mapped_column(default=datetime.utcnow) learned_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
last_referenced_at: Mapped[datetime | None] = mapped_column(default=None) last_referenced_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), default=None
)
# Relationships # Relationships
user: Mapped["User"] = relationship(back_populates="facts") user: Mapped["User"] = relationship(back_populates="facts")

View File

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

View File

@@ -3,7 +3,7 @@
import json import json
import logging import logging
import random import random
from datetime import datetime from datetime import datetime, timezone
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession 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), confidence=fact_data.get("confidence", 0.8),
source="auto_extraction", source="auto_extraction",
is_active=True, is_active=True,
learned_at=datetime.utcnow(), learned_at=datetime.now(timezone.utc),
# New fields from Living AI # New fields from Living AI
category=fact_data["type"], category=fact_data["type"],
importance=fact_data.get("importance", 0.5), importance=fact_data.get("importance", 0.5),

View File

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

View File

@@ -1,7 +1,7 @@
"""Opinion Service - manages bot opinion formation on topics.""" """Opinion Service - manages bot opinion formation on topics."""
import logging import logging
from datetime import datetime from datetime import datetime, timezone
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession 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.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( logger.debug(
f"Updated opinion on '{topic}': sentiment={opinion.sentiment:.2f}, " f"Updated opinion on '{topic}': sentiment={opinion.sentiment:.2f}, "

View File

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

View File

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

View File

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

View File

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

View File

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