forked from Rockachopa/Timmy-time-dashboard
feat: swarm E2E, MCP tools, timmy-serve L402, tests, notifications
Major Features: - Auto-spawn persona agents (Echo, Forge, Seer) on app startup - WebSocket broadcasts for real-time swarm UI updates - MCP tool integration: web search, file I/O, shell, Python execution - New /tools dashboard page showing agent capabilities - Real timmy-serve start with L402 payment gating middleware - Browser push notifications for briefings and task events Tests: - test_docker_agent.py: 9 tests for Docker agent runner - test_swarm_integration_full.py: 18 E2E lifecycle tests - Fixed all pytest warnings (436 tests, 0 warnings) Improvements: - Fixed coroutine warnings in coordinator broadcasts - Fixed ResourceWarning for unclosed process pipes - Added pytest-asyncio config to pyproject.toml - Test isolation with proper event loop cleanup
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
|
||||
@@ -21,6 +22,7 @@ from dashboard.routes.swarm_ws import router as swarm_ws_router
|
||||
from dashboard.routes.briefing import router as briefing_router
|
||||
from dashboard.routes.telegram import router as telegram_router
|
||||
from dashboard.routes.swarm_internal import router as swarm_internal_router
|
||||
from dashboard.routes.tools import router as tools_router
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
@@ -83,6 +85,18 @@ async def lifespan(app: FastAPI):
|
||||
rec["agents_offlined"],
|
||||
)
|
||||
|
||||
# Auto-spawn persona agents for a functional swarm (Echo, Forge, Seer)
|
||||
# Skip auto-spawning in test mode to avoid test isolation issues
|
||||
if os.environ.get("TIMMY_TEST_MODE") != "1":
|
||||
logger.info("Auto-spawning persona agents: Echo, Forge, Seer...")
|
||||
try:
|
||||
swarm_coordinator.spawn_persona("echo", agent_id="persona-echo")
|
||||
swarm_coordinator.spawn_persona("forge", agent_id="persona-forge")
|
||||
swarm_coordinator.spawn_persona("seer", agent_id="persona-seer")
|
||||
logger.info("Persona agents spawned successfully")
|
||||
except Exception as exc:
|
||||
logger.error("Failed to spawn persona agents: %s", exc)
|
||||
|
||||
# Auto-start Telegram bot if a token is configured
|
||||
from telegram_bot.bot import telegram_bot
|
||||
await telegram_bot.start()
|
||||
@@ -121,6 +135,7 @@ app.include_router(swarm_ws_router)
|
||||
app.include_router(briefing_router)
|
||||
app.include_router(telegram_router)
|
||||
app.include_router(swarm_internal_router)
|
||||
app.include_router(tools_router)
|
||||
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
|
||||
92
src/dashboard/routes/tools.py
Normal file
92
src/dashboard/routes/tools.py
Normal file
@@ -0,0 +1,92 @@
|
||||
"""Tools dashboard route — /tools endpoints.
|
||||
|
||||
Provides a dashboard page showing available tools, which agents have access
|
||||
to which tools, and usage statistics.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from swarm import registry as swarm_registry
|
||||
from swarm.personas import PERSONAS
|
||||
from timmy.tools import get_all_available_tools, get_tool_stats
|
||||
|
||||
router = APIRouter(tags=["tools"])
|
||||
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
||||
|
||||
|
||||
@router.get("/tools", response_class=HTMLResponse)
|
||||
async def tools_page(request: Request):
|
||||
"""Render the tools dashboard page."""
|
||||
# Get all available tools
|
||||
available_tools = get_all_available_tools()
|
||||
|
||||
# Get registered agents and their personas
|
||||
agents = swarm_registry.list_agents()
|
||||
agent_tools = []
|
||||
|
||||
for agent in agents:
|
||||
# Determine which tools this agent has based on its capabilities/persona
|
||||
tools_for_agent = []
|
||||
|
||||
# Check if it's a persona by name
|
||||
persona_id = None
|
||||
for pid, pdata in PERSONAS.items():
|
||||
if pdata["name"].lower() == agent.name.lower():
|
||||
persona_id = pid
|
||||
break
|
||||
|
||||
if persona_id:
|
||||
# Get tools for this persona
|
||||
for tool_id, tool_info in available_tools.items():
|
||||
if persona_id in tool_info["available_in"]:
|
||||
tools_for_agent.append({
|
||||
"id": tool_id,
|
||||
"name": tool_info["name"],
|
||||
"description": tool_info["description"],
|
||||
})
|
||||
elif agent.name.lower() == "timmy":
|
||||
# Timmy has all tools
|
||||
for tool_id, tool_info in available_tools.items():
|
||||
tools_for_agent.append({
|
||||
"id": tool_id,
|
||||
"name": tool_info["name"],
|
||||
"description": tool_info["description"],
|
||||
})
|
||||
|
||||
# Get tool stats for this agent
|
||||
stats = get_tool_stats(agent.id)
|
||||
|
||||
agent_tools.append({
|
||||
"id": agent.id,
|
||||
"name": agent.name,
|
||||
"status": agent.status,
|
||||
"tools": tools_for_agent,
|
||||
"stats": stats,
|
||||
})
|
||||
|
||||
# Calculate overall stats
|
||||
total_calls = sum(a["stats"]["total_calls"] for a in agent_tools if a["stats"])
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"tools.html",
|
||||
{
|
||||
"page_title": "Tools & Capabilities",
|
||||
"available_tools": available_tools,
|
||||
"agent_tools": agent_tools,
|
||||
"total_calls": total_calls,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tools/api/stats")
|
||||
async def tools_api_stats():
|
||||
"""Return tool usage statistics as JSON."""
|
||||
return {
|
||||
"all_stats": get_tool_stats(),
|
||||
"available_tools": list(get_all_available_tools().keys()),
|
||||
}
|
||||
@@ -24,8 +24,9 @@
|
||||
<a href="/briefing" class="mc-test-link">BRIEFING</a>
|
||||
<a href="/swarm/live" class="mc-test-link">SWARM</a>
|
||||
<a href="/marketplace/ui" class="mc-test-link">MARKET</a>
|
||||
<a href="/tools" class="mc-test-link">TOOLS</a>
|
||||
<a href="/mobile" class="mc-test-link">MOBILE</a>
|
||||
<a href="/mobile-test" class="mc-test-link">TEST</a>
|
||||
<button id="enable-notifications" class="mc-test-link" style="background:none;border:none;cursor:pointer;" title="Enable notifications">🔔</button>
|
||||
<span class="mc-time" id="clock"></span>
|
||||
</div>
|
||||
</header>
|
||||
@@ -44,5 +45,6 @@
|
||||
updateClock();
|
||||
</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc4s9bIOgUxi8T/jzmE6bgx5xwkVYG3WhIEOFSjBqg4X" crossorigin="anonymous"></script>
|
||||
<script src="/static/notifications.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
94
src/dashboard/templates/tools.html
Normal file
94
src/dashboard/templates/tools.html
Normal file
@@ -0,0 +1,94 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Tools & Capabilities — Mission Control{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<h1 class="display-6">🔧 Tools & Capabilities</h1>
|
||||
<p class="text-secondary">Agent tools and usage statistics</p>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="card bg-dark border-secondary">
|
||||
<div class="card-body text-center">
|
||||
<h3 class="mb-0">{{ total_calls }}</h3>
|
||||
<small class="text-secondary">Total Tool Calls</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Available Tools Reference -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h5 class="mb-3">Available Tools</h5>
|
||||
<div class="row g-3">
|
||||
{% for tool_id, tool_info in available_tools.items() %}
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 bg-dark border-secondary">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">{{ tool_info.name }}</h6>
|
||||
<p class="card-text small text-secondary">{{ tool_info.description }}</p>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Available to:</small>
|
||||
<div class="d-flex flex-wrap gap-1 mt-1">
|
||||
{% for persona in tool_info.available_in %}
|
||||
<span class="badge bg-secondary">{{ persona|title }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Agent Tool Assignments -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h5 class="mb-3">Agent Capabilities</h5>
|
||||
{% if agent_tools %}
|
||||
<div class="row g-3">
|
||||
{% for agent in agent_tools %}
|
||||
<div class="col-md-6">
|
||||
<div class="card bg-dark border-secondary">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span>
|
||||
<strong>{{ agent.name }}</strong>
|
||||
<span class="badge {% if agent.status == 'idle' %}bg-success{% elif agent.status == 'busy' %}bg-warning{% else %}bg-secondary{% endif %} ms-2">
|
||||
{{ agent.status }}
|
||||
</span>
|
||||
</span>
|
||||
{% if agent.stats %}
|
||||
<small class="text-muted">{{ agent.stats.total_calls }} calls</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if agent.tools %}
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
{% for tool in agent.tools %}
|
||||
<span class="badge bg-primary" title="{{ tool.description }}">
|
||||
{{ tool.name }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-secondary mb-0">No tools assigned</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-secondary">
|
||||
No agents registered yet.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user