Wire dashboard context into Hearthkeeper

This commit is contained in:
2026-05-12 10:28:33 -05:00
parent c1d6032eb2
commit 74e3c0dcaa
7 changed files with 117 additions and 31 deletions

View File

@@ -34,9 +34,18 @@ class HearthkeeperMode:
"""Determine if Hearthkeeper should generate a prompt.""" """Determine if Hearthkeeper should generate a prompt."""
return minutes_since_activity >= self.activity_threshold return minutes_since_activity >= self.activity_threshold
async def generate_prompt(self, theme: str | None = None) -> str: async def generate_prompt(
self,
theme: str | None = None,
dashboard: dict | None = None,
recent_discussion: list[str] | None = None,
) -> str:
"""Generate a gentle prompt for the stream.""" """Generate a gentle prompt for the stream."""
prompt = PromptTemplates.gentle_prompt(theme) prompt = PromptTemplates.gentle_prompt(
current_theme=theme,
dashboard=dashboard,
recent_discussion=recent_discussion or [],
)
response = await self.llm_client.generate(prompt) response = await self.llm_client.generate(prompt)
logger.info("Hearthkeeper generated gentle prompt") logger.info("Hearthkeeper generated gentle prompt")
return response return response

View File

@@ -80,6 +80,7 @@ class AgentOrchestrator:
"started_at": datetime.utcnow(), "started_at": datetime.utcnow(),
"message_count": 0, "message_count": 0,
"theme": None, "theme": None,
"dashboard": None,
"last_hearthkeeper_prompt_at": None, "last_hearthkeeper_prompt_at": None,
} }
self.chat_activity.record_activity(session_id) self.chat_activity.record_activity(session_id)
@@ -101,6 +102,7 @@ class AgentOrchestrator:
limit=1, limit=1,
) )
message_count = await repo.count_messages(session.id) message_count = await repo.count_messages(session.id)
dashboard = await repo.get_dashboard(session.id)
last_activity_at = ( last_activity_at = (
recent_messages[0].timestamp if recent_messages else session.started_at recent_messages[0].timestamp if recent_messages else session.started_at
) )
@@ -110,6 +112,7 @@ class AgentOrchestrator:
"started_at": session.started_at, "started_at": session.started_at,
"message_count": message_count, "message_count": message_count,
"theme": session.theme, "theme": session.theme,
"dashboard": Repository.serialize_dashboard(dashboard),
"last_hearthkeeper_prompt_at": None, "last_hearthkeeper_prompt_at": None,
} }
self.chat_activity.record_activity(session.id, occurred_at=last_activity_at) self.chat_activity.record_activity(session.id, occurred_at=last_activity_at)
@@ -363,15 +366,20 @@ class AgentOrchestrator:
if not session_info: if not session_info:
return None return None
recent_messages = [] active_chat_messages = []
recent_discussion_messages = []
async for db_session in get_session(): async for db_session in get_session():
repo = Repository(db_session) repo = Repository(db_session)
recent_messages = await repo.get_human_messages_since( active_chat_messages = await repo.get_human_messages_since(
session_id=session_id, session_id=session_id,
since=datetime.utcnow() - timedelta(minutes=1), since=datetime.utcnow() - timedelta(minutes=1),
) )
recent_discussion_messages = await repo.get_recent_human_messages(
session_id=session_id,
limit=5,
)
if self.response_suppression.should_suppress_response(len(recent_messages)): if self.response_suppression.should_suppress_response(len(active_chat_messages)):
return { return {
"session_id": session_id, "session_id": session_id,
"actions_taken": [], "actions_taken": [],
@@ -394,8 +402,13 @@ class AgentOrchestrator:
return None return None
try: try:
recent_discussion = [
message.content for message in recent_discussion_messages[:5]
]
agent_response = await self.hearthkeeper.generate_prompt( agent_response = await self.hearthkeeper.generate_prompt(
theme=session_info.get("theme") theme=session_info.get("theme"),
dashboard=session_info.get("dashboard"),
recent_discussion=recent_discussion,
) )
session_info["last_hearthkeeper_prompt_at"] = datetime.utcnow() session_info["last_hearthkeeper_prompt_at"] = datetime.utcnow()
delivery = await self.emit_agent_response( delivery = await self.emit_agent_response(
@@ -433,4 +446,5 @@ class AgentOrchestrator:
"message_count": session["message_count"], "message_count": session["message_count"],
"uptime_seconds": (datetime.utcnow() - session["started_at"]).total_seconds(), "uptime_seconds": (datetime.utcnow() - session["started_at"]).total_seconds(),
"theme": session.get("theme"), "theme": session.get("theme"),
"dashboard": session.get("dashboard"),
} }

View File

@@ -47,6 +47,9 @@ class MarkdownExporter:
actions = await repo.get_session_actions(session_id) actions = await repo.get_session_actions(session_id)
clips = await repo.get_clip_candidates(session_id) clips = await repo.get_clip_candidates(session_id)
seeds = await repo.get_blog_seeds(session_id) seeds = await repo.get_blog_seeds(session_id)
dashboard = await repo.get_dashboard(session_id)
dashboard_data = Repository.serialize_dashboard(dashboard)
# Build markdown # Build markdown
date = session.started_at.strftime("%Y-%m-%d") date = session.started_at.strftime("%Y-%m-%d")
@@ -59,7 +62,20 @@ class MarkdownExporter:
# Stream Theme # Stream Theme
ledger += "## Stream Theme\n" ledger += "## Stream Theme\n"
if session.theme: if dashboard_data:
if dashboard_data.get("stream_title"):
ledger += f"**Title:** {dashboard_data['stream_title']}\n"
if dashboard_data.get("game"):
ledger += f"**Game:** {dashboard_data['game']}\n"
if dashboard_data.get("mood"):
ledger += f"**Mood:** {dashboard_data['mood']}\n"
if dashboard_data.get("content_angle"):
ledger += f"**Content Angle:** {dashboard_data['content_angle']}\n"
if dashboard_data.get("session_goals"):
ledger += "\n**Session Goals**\n"
for goal in dashboard_data["session_goals"]:
ledger += f"- {goal}\n"
elif session.theme:
ledger += f"{session.theme}\n" ledger += f"{session.theme}\n"
else: else:
ledger += "*No theme recorded*\n" ledger += "*No theme recorded*\n"

View File

@@ -106,6 +106,12 @@ class LLMClient:
""" """
logger.debug(f"Mock generation for prompt: {prompt[:50]}...") logger.debug(f"Mock generation for prompt: {prompt[:50]}...")
if "content angle:" in prompt.lower():
for line in prompt.splitlines():
if line.lower().startswith("content angle:"):
angle = line.split(":", 1)[1].strip()
return f"The quiet here keeps circling back to {angle}."
# Simple deterministic responses for testing # Simple deterministic responses for testing
if "hello" in prompt.lower(): if "hello" in prompt.lower():
return "Greetings, traveler! Welcome to The Sanctum." return "Greetings, traveler! Welcome to The Sanctum."

View File

@@ -7,11 +7,37 @@ class PromptTemplates:
"""Collection of prompt templates for different modes.""" """Collection of prompt templates for different modes."""
@staticmethod @staticmethod
def gentle_prompt(current_theme: Optional[str] = None) -> str: def gentle_prompt(
current_theme: Optional[str] = None,
dashboard: Optional[dict] = None,
recent_discussion: Optional[list[str]] = None,
) -> str:
"""Generate a gentle prompt when chat has been inactive.""" """Generate a gentle prompt when chat has been inactive."""
dashboard = dashboard or {}
recent_discussion = recent_discussion or []
context_lines = [
"Generate one brief Hearthkeeper prompt for a calm, reflective livestream.",
"The prompt must be restrained, thematic, and not engagement bait.",
]
if dashboard.get("stream_title"):
context_lines.append(f"Stream title: {dashboard['stream_title']}")
if dashboard.get("game"):
context_lines.append(f"Game: {dashboard['game']}")
if dashboard.get("mood"):
context_lines.append(f"Mood: {dashboard['mood']}")
if dashboard.get("content_angle"):
context_lines.append(f"Content angle: {dashboard['content_angle']}")
if dashboard.get("session_goals"):
goals = "; ".join(dashboard["session_goals"])
context_lines.append(f"Session goals: {goals}")
if current_theme: if current_theme:
return f"Gently prompt the chat about: {current_theme}" context_lines.append(f"Current theme: {current_theme}")
return "Generate a gentle, inviting prompt to encourage discussion in the stream." if recent_discussion:
context_lines.append("Recent human discussion:")
context_lines.extend(f"- {message}" for message in recent_discussion[:5])
return "\n".join(context_lines)
@staticmethod @staticmethod
def steward_response(message: str, context: Optional[str] = None) -> str: def steward_response(message: str, context: Optional[str] = None) -> str:

View File

@@ -1,7 +1,6 @@
"""FastAPI main application.""" """FastAPI main application."""
import asyncio import asyncio
import json
import secrets import secrets
from contextlib import suppress from contextlib import suppress
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
@@ -210,26 +209,7 @@ async def test_loop_inactivity(
def serialize_dashboard(dashboard) -> dict: def serialize_dashboard(dashboard) -> dict:
"""Serialize a dashboard database model into an API response.""" """Serialize a dashboard database model into an API response."""
session_goals = [] return Repository.serialize_dashboard(dashboard)
if dashboard.session_goals:
try:
session_goals = json.loads(dashboard.session_goals)
except json.JSONDecodeError:
session_goals = []
return {
"session_id": dashboard.session_id,
"raw_markdown": dashboard.raw_markdown,
"stream_title": dashboard.stream_title,
"game": dashboard.game,
"mood": dashboard.mood,
"go_live_notification": dashboard.go_live_notification,
"social_post": dashboard.social_post,
"session_goals": session_goals,
"content_angle": dashboard.content_angle,
"created_at": dashboard.created_at.isoformat(),
"updated_at": dashboard.updated_at.isoformat(),
}
@app.post("/admin/session/dashboard", dependencies=[Depends(require_admin)]) @app.post("/admin/session/dashboard", dependencies=[Depends(require_admin)])
@@ -253,6 +233,14 @@ async def save_session_dashboard(request: DashboardRequest) -> dict:
content_angle=request.content_angle, content_angle=request.content_angle,
) )
if orchestrator and request.session_id in orchestrator.active_sessions:
orchestrator.active_sessions[request.session_id]["dashboard"] = (
serialize_dashboard(dashboard)
)
orchestrator.active_sessions[request.session_id]["theme"] = (
request.content_angle or request.stream_title
)
return { return {
"status": "dashboard_saved", "status": "dashboard_saved",
"dashboard": serialize_dashboard(dashboard), "dashboard": serialize_dashboard(dashboard),

View File

@@ -123,6 +123,33 @@ class Repository:
result = await self.session.execute(stmt) result = await self.session.execute(stmt)
return result.scalars().first() return result.scalars().first()
@staticmethod
def serialize_dashboard(dashboard: StreamDashboard | None) -> dict | None:
"""Serialize a dashboard model into a plain dict."""
if dashboard is None:
return None
session_goals = []
if dashboard.session_goals:
try:
session_goals = json.loads(dashboard.session_goals)
except json.JSONDecodeError:
session_goals = []
return {
"session_id": dashboard.session_id,
"raw_markdown": dashboard.raw_markdown,
"stream_title": dashboard.stream_title,
"game": dashboard.game,
"mood": dashboard.mood,
"go_live_notification": dashboard.go_live_notification,
"social_post": dashboard.social_post,
"session_goals": session_goals,
"content_angle": dashboard.content_angle,
"created_at": dashboard.created_at.isoformat(),
"updated_at": dashboard.updated_at.isoformat(),
}
# Chat Message operations # Chat Message operations
async def add_chat_message( async def add_chat_message(