forked from Rockachopa/Timmy-time-dashboard
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:
@@ -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:
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user