AI generated first iteration

This commit is contained in:
2026-05-11 15:01:55 -05:00
parent af3e282fda
commit 412d7caec3
28 changed files with 2094 additions and 157 deletions

1
app/agent/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Agent module exports."""

View File

@@ -0,0 +1 @@
"""Agent modes module."""

View File

@@ -0,0 +1,47 @@
"""Hearthkeeper Mode - Nurtures stream warmth and gentle presence."""
import logging
from app.llm.client import LLMClient
from app.llm.prompts import PromptTemplates
logger = logging.getLogger(__name__)
class HearthkeeperMode:
"""
Hearthkeeper - The gentle voice of the Sanctum.
Purpose:
- Maintains the emotional warmth of the stream
- Generates gentle prompts when chat is quiet
- Encourages participation and connection
- Never forced or aggressive
Policy:
- Activates when no human chat for 15+ minutes
- Generates 1-2 sentence conversation starters
- Respects stream theme if established
- Can be suppressed if chat becomes active
"""
def __init__(self, llm_client: LLMClient):
"""Initialize Hearthkeeper mode."""
self.llm_client = llm_client
self.last_activity_minutes = 0
self.activity_threshold = 15
async def should_activate(self, minutes_since_activity: int) -> bool:
"""Determine if Hearthkeeper should generate a prompt."""
return minutes_since_activity >= self.activity_threshold
async def generate_prompt(self, theme: str | None = None) -> str:
"""Generate a gentle prompt for the stream."""
prompt = PromptTemplates.gentle_prompt(theme)
response = await self.llm_client.generate(prompt)
logger.info("Hearthkeeper generated gentle prompt")
return response
async def on_chat_activity(self) -> None:
"""React to new chat activity."""
logger.debug("Hearthkeeper notes renewed activity")
self.last_activity_minutes = 0

View File

@@ -0,0 +1,64 @@
"""Librarian Mode - Archives and categorizes important discussion."""
import logging
from app.llm.client import LLMClient
from app.llm.prompts import PromptTemplates
logger = logging.getLogger(__name__)
class LibrarianMode:
"""
Librarian - The keeper of knowledge and archives.
Purpose:
- Identifies and catalogs important discussion points
- Creates summaries of key topics
- Builds context for future reference
- Prepares data for blog and clip exports
Policy:
- Runs passively, always monitoring
- Tags messages by topic/sentiment
- Creates discussion threads
- Identifies "clip-worthy" moments
- Feeds data to Scribe for final export
"""
def __init__(self, llm_client: LLMClient):
"""Initialize Librarian mode."""
self.llm_client = llm_client
self.archived_messages: list[dict] = []
self.topics: dict[str, list[str]] = {}
async def archive_message(self, message_id: str, content: str, username: str) -> None:
"""Archive an important message."""
self.archived_messages.append(
{
"id": message_id,
"content": content,
"username": username,
}
)
logger.debug(f"Librarian archived message from {username}")
async def identify_topics(self, messages: list[str]) -> list[str]:
"""Identify key topics from a set of messages."""
# Placeholder: Would use LLM to extract topics
topics = ["general", "technical", "community"]
return topics
async def create_summary(self, topic: str, messages: list[str]) -> str:
"""Create a summary of messages under a topic."""
prompt = PromptTemplates.librarian_summary(messages)
summary = await self.llm_client.generate(prompt, max_tokens=300)
logger.info(f"Librarian created summary for topic: {topic}")
return summary
async def get_archives(self) -> dict:
"""Get the archive status."""
return {
"mode": "librarian",
"archived_messages": len(self.archived_messages),
"topics_tracked": len(self.topics),
}

73
app/agent/modes/scribe.py Normal file
View File

@@ -0,0 +1,73 @@
"""Scribe Mode - Generates the post-stream markdown ledger."""
import logging
from datetime import datetime
from app.llm.client import LLMClient
logger = logging.getLogger(__name__)
class ScribeMode:
"""
Scribe - The chronicler of the stream's story.
Purpose:
- Compiles session data into a markdown ledger
- Generates blog post seeds
- Identifies clip candidates
- Exports final summary document
Policy:
- Activates at end of stream
- Reads data from Librarian, Warden, and repository
- Creates structured markdown output
- Organizes clips and blog topics
- Ready for post-processing or publishing
"""
def __init__(self, llm_client: LLMClient):
"""Initialize Scribe mode."""
self.llm_client = llm_client
self.ledger_entries: list[str] = []
async def add_entry(self, section: str, content: str) -> None:
"""Add an entry to the ledger."""
self.ledger_entries.append(f"## {section}\n{content}")
logger.debug(f"Scribe recorded ledger entry: {section}")
async def compile_ledger(
self,
theme: str,
discussion_points: list[str],
agent_actions: list[str],
clip_candidates: list[str],
blog_seeds: list[str],
) -> str:
"""Compile all data into a markdown ledger."""
date = datetime.utcnow().strftime("%Y-%m-%d")
ledger = f"# Sanctum Ledger — {date}\n\n"
ledger += f"## Stream Theme\n{theme}\n\n"
ledger += f"## Notable Discussion\n"
for point in discussion_points:
ledger += f"- {point}\n"
ledger += "\n"
ledger += f"## Agent Actions\n"
for action in agent_actions:
ledger += f"- {action}\n"
ledger += "\n"
ledger += f"## Clip Candidates\n"
for clip in clip_candidates:
ledger += f"- {clip}\n"
ledger += "\n"
ledger += f"## Blog Seeds\n"
for seed in blog_seeds:
ledger += f"- {seed}\n"
logger.info("Scribe compiled stream ledger")
return ledger
async def export_ledger(self, filename: str, content: str) -> None:
"""Export ledger to file."""
# Actual export handled by MarkdownExporter
logger.info(f"Scribe prepared ledger for export: {filename}")

View File

@@ -0,0 +1,51 @@
"""Steward Mode - Responds to chat with knowledge and warmth."""
import logging
from app.llm.client import LLMClient
from app.llm.prompts import PromptTemplates
logger = logging.getLogger(__name__)
class StewardMode:
"""
Steward - The thoughtful keeper of conversation.
Purpose:
- Responds to direct questions and comments
- Shares relevant knowledge and context
- Maintains conversation continuity
- Balances speaking and listening
Policy:
- Activates when chat is active
- Only responds to messages explicitly mentioning the bot
- Keeps responses brief (1-3 sentences)
- Never interrupts human conversation flow
- Can escalate to other modes if needed
"""
def __init__(self, llm_client: LLMClient):
"""Initialize Steward mode."""
self.llm_client = llm_client
self.response_count = 0
self.max_responses_per_minute = 2
async def should_respond(self, message: str, is_mention: bool) -> bool:
"""Determine if Steward should respond."""
# Only respond to mentions for now (can be expanded)
return is_mention and self.response_count < self.max_responses_per_minute
async def generate_response(
self, message: str, context: str | None = None
) -> str:
"""Generate a thoughtful response to a message."""
prompt = PromptTemplates.steward_response(message, context)
response = await self.llm_client.generate(prompt, max_tokens=150)
self.response_count += 1
logger.info("Steward generated response")
return response
async def on_response_sent(self) -> None:
"""Record that a response was sent."""
logger.debug("Steward response recorded")

85
app/agent/modes/warden.py Normal file
View File

@@ -0,0 +1,85 @@
"""Warden Mode - Detects and flags suspicious content."""
import logging
from app.llm.client import LLMClient
from app.llm.prompts import PromptTemplates
logger = logging.getLogger(__name__)
class WardenMode:
"""
Warden - The guardian against unwanted influences.
Purpose:
- Detects suspicious patterns (spam, scams, bot activity)
- Flags Discord growth schemes and link spam
- Monitors for manipulation or harmful content
- Provides data for moderation decisions
Policy:
- Runs on every message (always active)
- Never takes action directly (only flags)
- Patterns to detect:
* "Join our Discord"
* "Grow your channel"
* Multiple links
* Repeated messages (spam)
* Known scam keywords
- Flags are recorded for human review
"""
def __init__(self, llm_client: LLMClient):
"""Initialize Warden mode."""
self.llm_client = llm_client
self.suspicious_patterns = [
"join our discord",
"discord.gg",
"grow your channel",
"easy money",
"click here",
"limited offer",
"act now",
]
self.flagged_count = 0
async def analyze_message(self, message: str) -> dict:
"""Analyze a message for suspicious content."""
result = {
"is_suspicious": False,
"patterns_detected": [],
"severity": "safe",
}
# Simple pattern matching
message_lower = message.lower()
for pattern in self.suspicious_patterns:
if pattern in message_lower:
result["patterns_detected"].append(pattern)
result["is_suspicious"] = True
# Check for multiple links
link_count = message.count("http") + message.count("www")
if link_count > 1:
result["patterns_detected"].append("multiple_links")
result["is_suspicious"] = True
# Determine severity
if result["is_suspicious"]:
if len(result["patterns_detected"]) >= 2:
result["severity"] = "high"
else:
result["severity"] = "medium"
self.flagged_count += 1
logger.warning(
f"Warden flagged suspicious message: {result['patterns_detected']}"
)
return result
async def get_report(self) -> dict:
"""Get Warden's activity report."""
return {
"mode": "warden",
"total_flagged": self.flagged_count,
}

211
app/agent/orchestrator.py Normal file
View File

@@ -0,0 +1,211 @@
"""Agent Orchestrator - Routes messages and manages agent modes."""
import logging
import uuid
from datetime import datetime
from sqlalchemy.ext.asyncio import AsyncSession
from app.agent.policies import (
ChatActivityPolicy,
ResponseSuppression,
SuspiciousContentPolicy,
)
from app.agent.modes.hearthkeeper import HearthkeeperMode
from app.agent.modes.steward import StewardMode
from app.agent.modes.warden import WardenMode
from app.agent.modes.librarian import LibrarianMode
from app.agent.modes.scribe import ScribeMode
from app.llm.client import LLMClient
from app.memory.database import async_session_factory
from app.memory.models import AgentActionType
from app.memory.repository import Repository
logger = logging.getLogger(__name__)
class AgentOrchestrator:
"""
Main orchestrator for agent behavior.
Routes chat messages to appropriate modes and manages responses.
Implements policies for when to speak, when to stay silent,
and how to flag suspicious content.
"""
def __init__(self):
"""Initialize the orchestrator and all modes."""
self.llm_client = LLMClient()
# Initialize modes
self.hearthkeeper = HearthkeeperMode(self.llm_client)
self.steward = StewardMode(self.llm_client)
self.warden = WardenMode(self.llm_client)
self.librarian = LibrarianMode(self.llm_client)
self.scribe = ScribeMode(self.llm_client)
# Initialize policies
self.chat_activity = ChatActivityPolicy(inactivity_threshold_minutes=15)
self.response_suppression = ResponseSuppression()
self.suspicious_content = SuspiciousContentPolicy()
# Track active sessions
self.active_sessions: dict[str, dict] = {}
logger.info("AgentOrchestrator initialized with all modes and policies")
async def start_session(self, channel_name: str) -> str:
"""
Start a new stream session.
Args:
channel_name: Twitch channel name
Returns:
Session ID
"""
session_id = str(uuid.uuid4())
async with async_session_factory() as db_session:
repo = Repository(db_session)
await repo.create_session(channel_name)
self.active_sessions[session_id] = {
"channel_name": channel_name,
"started_at": datetime.utcnow(),
"message_count": 0,
"theme": None,
}
logger.info(f"Started session {session_id} for {channel_name}")
return session_id
async def end_session(self, session_id: str) -> None:
"""
End a stream session and trigger ledger generation.
Args:
session_id: Session ID
"""
if session_id not in self.active_sessions:
logger.warning(f"Session {session_id} not found")
return
async with async_session_factory() as db_session:
repo = Repository(db_session)
await repo.end_session(session_id)
del self.active_sessions[session_id]
logger.info(f"Ended session {session_id}")
async def handle_chat_message(
self,
session_id: str,
username: str,
message: str,
) -> dict:
"""
Process a chat message and determine agent response.
Args:
session_id: Session ID
username: Username of message sender
message: Message content
Returns:
Response dict with agent_response, actions_taken, etc.
"""
if session_id not in self.active_sessions:
logger.warning(f"Session {session_id} not found")
return {"agent_response": None, "actions_taken": []}
session_info = self.active_sessions[session_id]
actions = []
agent_response = None
async with async_session_factory() as db_session:
repo = Repository(db_session)
# Store the message
message_id = await repo.add_chat_message(
session_id=session_id,
username=username,
content=message,
is_bot=False,
)
# Record activity
self.chat_activity.record_activity(session_id)
session_info["message_count"] += 1
# 1. Warden always analyzes (passive mode)
warden_result = await self.warden.analyze_message(message)
if warden_result["is_suspicious"]:
actions.append(f"WARDEN_FLAG: {warden_result['severity']}")
async with async_session_factory() as db_session:
repo = Repository(db_session)
await repo.record_action(
session_id=session_id,
action_type=AgentActionType.FLAG_SUSPICIOUS,
mode="warden",
description=f"Detected: {warden_result['patterns_detected']}",
triggered_by_message_id=message_id,
)
# 2. Check if we should suppress responses due to active chat
recent_messages = []
async with async_session_factory() as db_session:
repo = Repository(db_session)
recent_messages = await repo.get_recent_messages(session_id, limit=10)
if self.response_suppression.should_suppress_response(len(recent_messages)):
logger.debug("Response suppressed due to active chat")
return {
"agent_response": agent_response,
"actions_taken": actions,
}
# 3. Hearthkeeper: Generate prompt if chat inactive
if self.chat_activity.should_hearthkeeper_prompt(session_id):
try:
agent_response = await self.hearthkeeper.generate_prompt(
theme=session_info.get("theme")
)
actions.append("HEARTHKEEPER_PROMPT")
async with async_session_factory() as db_session:
repo = Repository(db_session)
await repo.record_action(
session_id=session_id,
action_type=AgentActionType.RESPONSE,
mode="hearthkeeper",
description=agent_response,
)
except Exception as e:
logger.error(f"Error in Hearthkeeper: {e}")
# 4. Librarian: Archive important messages (passive)
if len(message) > 50: # Archive longer messages
await self.librarian.archive_message(message_id, message, username)
logger.info(
f"Message processed. Session: {session_id}, Actions: {actions}"
)
return {
"agent_response": agent_response,
"actions_taken": actions,
}
async def get_session_status(self, session_id: str) -> dict:
"""Get status of a session."""
if session_id not in self.active_sessions:
return {}
session = self.active_sessions[session_id]
return {
"session_id": session_id,
"channel_name": session["channel_name"],
"message_count": session["message_count"],
"uptime_seconds": (datetime.utcnow() - session["started_at"]).total_seconds(),
"theme": session.get("theme"),
}

105
app/agent/policies.py Normal file
View File

@@ -0,0 +1,105 @@
"""Agent behavior policies and rules."""
import logging
from datetime import datetime, timedelta
logger = logging.getLogger(__name__)
class ChatActivityPolicy:
"""Policy for detecting chat activity and inactivity periods."""
def __init__(self, inactivity_threshold_minutes: int = 15):
"""
Initialize policy.
Args:
inactivity_threshold_minutes: Minutes of no chat before Hearthkeeper activates
"""
self.inactivity_threshold = timedelta(minutes=inactivity_threshold_minutes)
self.last_message_time: dict[str, datetime] = {}
def record_activity(self, session_id: str) -> None:
"""Record that chat activity occurred."""
self.last_message_time[session_id] = datetime.utcnow()
def minutes_since_activity(self, session_id: str) -> int:
"""Get minutes since last chat message."""
if session_id not in self.last_message_time:
return 0
elapsed = datetime.utcnow() - self.last_message_time[session_id]
return int(elapsed.total_seconds() / 60)
def should_hearthkeeper_prompt(self, session_id: str) -> bool:
"""Determine if Hearthkeeper should send a prompt."""
minutes = self.minutes_since_activity(session_id)
should = minutes >= self.inactivity_threshold.total_seconds() / 60
if should:
logger.info(f"Chat inactive for {minutes} minutes. Hearthkeeper may prompt.")
return should
class ResponseSuppression:
"""Policy for when the agent should NOT respond."""
# Suppress responses when chat is very active (humans are talking)
ACTIVE_CHAT_THRESHOLD = 5 # 5+ messages per minute = suppress
@staticmethod
def should_suppress_response(recent_message_count: int, time_window_minutes: int = 1) -> bool:
"""
Determine if agent should stay silent due to active chat.
Args:
recent_message_count: Number of messages in the time window
time_window_minutes: Time window in minutes
Returns:
True if agent should suppress response
"""
messages_per_minute = recent_message_count / time_window_minutes
suppress = messages_per_minute >= ResponseSuppression.ACTIVE_CHAT_THRESHOLD
if suppress:
logger.debug(f"Response suppressed due to active chat ({messages_per_minute:.1f} msg/min)")
return suppress
class SuspiciousContentPolicy:
"""Policy for detecting suspicious content."""
# Patterns that raise Warden alerts
SUSPICIOUS_KEYWORDS = [
"join our discord",
"discord.gg",
"grow your channel",
"easy money",
"limited offer",
]
@staticmethod
def is_suspicious(message: str) -> bool:
"""
Check if a message matches suspicious patterns.
Args:
message: Message content
Returns:
True if message is suspicious
"""
message_lower = message.lower()
for keyword in SuspiciousContentPolicy.SUSPICIOUS_KEYWORDS:
if keyword in message_lower:
logger.warning(f"Suspicious content detected: {keyword}")
return True
# Check for multiple links
link_count = message.count("http") + message.count("www")
if link_count > 1:
logger.warning("Suspicious content detected: multiple links")
return True
return False