Wire dashboard context into Hearthkeeper
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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:
|
||||
|
||||
30
app/main.py
30
app/main.py
@@ -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),
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user