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."""
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."""
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)
logger.info("Hearthkeeper generated gentle prompt")
return response

View File

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

View File

@@ -47,6 +47,9 @@ class MarkdownExporter:
actions = await repo.get_session_actions(session_id)
clips = await repo.get_clip_candidates(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
date = session.started_at.strftime("%Y-%m-%d")
@@ -59,7 +62,20 @@ class MarkdownExporter:
# Stream Theme
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"
else:
ledger += "*No theme recorded*\n"

View File

@@ -106,6 +106,12 @@ class LLMClient:
"""
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
if "hello" in prompt.lower():
return "Greetings, traveler! Welcome to The Sanctum."

View File

@@ -7,11 +7,37 @@ class PromptTemplates:
"""Collection of prompt templates for different modes."""
@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."""
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:
return f"Gently prompt the chat about: {current_theme}"
return "Generate a gentle, inviting prompt to encourage discussion in the stream."
context_lines.append(f"Current theme: {current_theme}")
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
def steward_response(message: str, context: Optional[str] = None) -> str:

View File

@@ -1,7 +1,6 @@
"""FastAPI main application."""
import asyncio
import json
import secrets
from contextlib import suppress
from pydantic import BaseModel, Field
@@ -210,26 +209,7 @@ async def test_loop_inactivity(
def serialize_dashboard(dashboard) -> dict:
"""Serialize a dashboard database model into an API response."""
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(),
}
return Repository.serialize_dashboard(dashboard)
@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,
)
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 {
"status": "dashboard_saved",
"dashboard": serialize_dashboard(dashboard),

View File

@@ -123,6 +123,33 @@ class Repository:
result = await self.session.execute(stmt)
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
async def add_chat_message(