Merge pull request #51 from AlexanderWhitestone/feature/task-queue-and-ui-fixes

feat: wire chat-to-task-queue and briefing integration
This commit is contained in:
Alexander Whitestone
2026-02-26 11:31:25 -05:00
committed by GitHub
3 changed files with 164 additions and 4 deletions

View File

@@ -1,3 +1,5 @@
import logging
import re
from datetime import datetime
from pathlib import Path
@@ -8,9 +10,35 @@ from fastapi.templating import Jinja2Templates
from timmy.session import chat as timmy_chat
from dashboard.store import message_log
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/agents", tags=["agents"])
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
# ── Task queue detection ──────────────────────────────────────────────────
# Patterns that indicate the user wants to queue a task rather than chat
_QUEUE_PATTERNS = [
re.compile(r"\b(?:add|put|schedule|queue|submit)\b.*\b(?:to the|on the|in the)?\s*(?:queue|task(?:\s*queue)?|task list)\b", re.IGNORECASE),
re.compile(r"\bschedule\s+(?:this|that|a)\b", re.IGNORECASE),
re.compile(r"\bcreate\s+(?:a\s+)?task\b", re.IGNORECASE),
]
def _extract_task_from_message(message: str) -> dict | None:
"""If the message looks like a task-queue request, return task details."""
for pattern in _QUEUE_PATTERNS:
if pattern.search(message):
# Strip the queue instruction to get the actual task description
title = re.sub(
r"\b(?:add|put|schedule|queue|submit|create)\b.*?\b(?:to the|on the|in the|a)?\s*(?:queue|task(?:\s*queue)?|task list)\b",
"", message, flags=re.IGNORECASE,
).strip(" ,:;-")
# If stripping removed everything, use the full message
if not title or len(title) < 5:
title = message
return {"title": title[:120], "description": message}
return None
# Static metadata for known agents — enriched onto live registry entries.
_AGENT_METADATA: dict[str, dict] = {
"timmy": {
@@ -74,10 +102,36 @@ async def chat_timmy(request: Request, message: str = Form(...)):
response_text = None
error_text = None
try:
response_text = timmy_chat(message)
except Exception as exc:
error_text = f"Timmy is offline: {exc}"
# Check if the user wants to queue a task instead of chatting
task_info = _extract_task_from_message(message)
if task_info:
try:
from task_queue.models import create_task
task = create_task(
title=task_info["title"],
description=task_info["description"],
created_by="user",
assigned_to="timmy",
priority="normal",
requires_approval=True,
)
response_text = (
f"Task queued for approval: **{task.title}**\n\n"
f"Status: `{task.status.value}` | "
f"[View Task Queue](/tasks)"
)
logger.info("Chat → task queue: %s (id=%s)", task.title, task.id)
except Exception as exc:
logger.error("Failed to create task from chat: %s", exc)
# Fall through to normal chat if task creation fails
task_info = None
# Normal chat path (also used as fallback if task creation failed)
if not task_info:
try:
response_text = timmy_chat(message)
except Exception as exc:
error_text = f"Timmy is offline: {exc}"
message_log.append(role="user", content=message, timestamp=timestamp)
if response_text is not None:

View File

@@ -166,6 +166,30 @@ def _gather_swarm_summary(since: datetime) -> str:
return "Swarm data unavailable."
def _gather_task_queue_summary() -> str:
"""Pull task queue stats for the briefing. Graceful if unavailable."""
try:
from task_queue.models import get_task_summary_for_briefing
stats = get_task_summary_for_briefing()
parts = []
if stats["pending_approval"]:
parts.append(f"{stats['pending_approval']} task(s) pending approval")
if stats["running"]:
parts.append(f"{stats['running']} task(s) running")
if stats["completed"]:
parts.append(f"{stats['completed']} task(s) completed")
if stats["failed"]:
parts.append(f"{stats['failed']} task(s) failed")
for fail in stats.get("recent_failures", []):
parts.append(f" - Failed: {fail['title']}")
if stats["vetoed"]:
parts.append(f"{stats['vetoed']} task(s) vetoed")
return "; ".join(parts) if parts else "No tasks in the queue."
except Exception as exc:
logger.debug("Task queue summary error: %s", exc)
return "Task queue data unavailable."
def _gather_chat_summary(since: datetime) -> str:
"""Pull recent chat messages from the in-memory log."""
try:
@@ -213,16 +237,20 @@ class BriefingEngine:
swarm_info = _gather_swarm_summary(period_start)
chat_info = _gather_chat_summary(period_start)
task_info = _gather_task_queue_summary()
prompt = (
"You are Timmy, a sovereign local AI companion.\n"
"Here is what happened since the last briefing:\n\n"
f"SWARM ACTIVITY:\n{swarm_info}\n\n"
f"TASK QUEUE:\n{task_info}\n\n"
f"RECENT CONVERSATIONS:\n{chat_info}\n\n"
"Summarize the last period of activity into a 5-minute morning briefing. "
"Be concise, warm, and direct. "
"Use plain prose — no bullet points. "
"Maximum 300 words. "
"If there are tasks pending approval, mention them prominently. "
"If there are failed tasks, flag them as needing attention. "
"End with a short paragraph listing any items that need the owner's approval, "
"or say 'No approvals needed today.' if there are none."
)

View File

@@ -304,3 +304,81 @@ def test_api_approve_nonexistent(client):
def test_api_veto_nonexistent(client):
resp = client.patch("/api/tasks/nonexistent/veto")
assert resp.status_code == 404
# ── Chat → Task Queue Integration ─────────────────────────────────────────
def test_chat_queue_detection_add_to_queue():
"""'add X to the queue' should be detected as a task request."""
from dashboard.routes.agents import _extract_task_from_message
result = _extract_task_from_message("add run the tests to the task queue")
assert result is not None
assert "title" in result
assert "description" in result
def test_chat_queue_detection_schedule():
"""'schedule this' should be detected as a task request."""
from dashboard.routes.agents import _extract_task_from_message
result = _extract_task_from_message("schedule this for later")
assert result is not None
def test_chat_queue_detection_create_task():
"""'create a task' should be detected."""
from dashboard.routes.agents import _extract_task_from_message
result = _extract_task_from_message("create a task to refactor the login page")
assert result is not None
assert "refactor" in result["title"].lower()
def test_chat_queue_detection_normal_message():
"""Normal messages should NOT be detected as task requests."""
from dashboard.routes.agents import _extract_task_from_message
assert _extract_task_from_message("hello how are you") is None
assert _extract_task_from_message("what is the weather today") is None
assert _extract_task_from_message("tell me a joke") is None
def test_chat_creates_task_on_queue_request(client):
"""Posting 'add X to the queue' via chat should create a task."""
with patch("dashboard.routes.agents.timmy_chat") as mock_chat:
mock_chat.return_value = "Sure, I'll do that."
resp = client.post(
"/agents/timmy/chat",
data={"message": "add deploy the new feature to the task queue"},
)
assert resp.status_code == 200
assert "Task queued" in resp.text or "task queue" in resp.text.lower()
# timmy_chat should NOT have been called — task was intercepted
mock_chat.assert_not_called()
def test_chat_normal_message_uses_timmy(client):
"""Normal messages should go through to Timmy as usual."""
with patch("dashboard.routes.agents.timmy_chat") as mock_chat:
mock_chat.return_value = "Hello there!"
resp = client.post(
"/agents/timmy/chat",
data={"message": "hello how are you"},
)
assert resp.status_code == 200
mock_chat.assert_called_once()
# ── Briefing Integration ──────────────────────────────────────────────────
def test_briefing_task_queue_summary():
"""Briefing engine should include task queue data."""
from task_queue.models import create_task
from timmy.briefing import _gather_task_queue_summary
create_task(title="Briefing integration test", created_by="test")
summary = _gather_task_queue_summary()
assert "pending" in summary.lower() or "task" in summary.lower()