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