diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 39bd62d1..6889dbb6 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -39,6 +39,14 @@ services: - ./src:/app/src:ro - ./tests:/app/tests:ro - ./static:/app/static:ro + - ./hands:/app/hands:ro + - ./docker:/app/docker:ro + - ./Dockerfile:/app/Dockerfile:ro + - ./docker-compose.yml:/app/docker-compose.yml:ro + - ./docker-compose.dev.yml:/app/docker-compose.dev.yml:ro + - ./docker-compose.prod.yml:/app/docker-compose.prod.yml:ro + - ./docker-compose.test.yml:/app/docker-compose.test.yml:ro + - ./docker-compose.microservices.yml:/app/docker-compose.microservices.yml:ro - ./pyproject.toml:/app/pyproject.toml:ro - test-data:/app/data environment: @@ -67,6 +75,7 @@ services: volumes: - ./src:/app/src:ro - ./static:/app/static:ro + - ./hands:/app/hands:ro - test-data:/app/data environment: DEBUG: "true" @@ -117,6 +126,7 @@ services: - agents volumes: - ./src:/app/src:ro + - ./hands:/app/hands:ro - test-data:/app/data environment: COORDINATOR_URL: "http://dashboard:8000" diff --git a/docker/Dockerfile.test b/docker/Dockerfile.test index 5db3078e..602ac2d1 100644 --- a/docker/Dockerfile.test +++ b/docker/Dockerfile.test @@ -39,7 +39,7 @@ FROM python:3.12-slim WORKDIR /app RUN apt-get update && apt-get install -y --no-install-recommends \ - curl git \ + curl git fontconfig fonts-dejavu-core \ && rm -rf /var/lib/apt/lists/* # Copy installed packages from builder @@ -48,7 +48,14 @@ COPY --from=builder /usr/local/lib/python3.12/site-packages \ COPY --from=builder /usr/local/bin /usr/local/bin # Create directories for bind mounts -RUN mkdir -p /app/src /app/tests /app/static /app/data +RUN mkdir -p /app/src /app/tests /app/static /app/hands /app/data /app/docker + +# Initialize a minimal git repo so git-dependent code (GitSafety, repo_root +# detection) works correctly inside the container. +RUN git config --global user.email "timmy@test" \ + && git config --global user.name "Timmy Test" \ + && git init /app \ + && git -C /app commit --allow-empty -m "init" ENV PYTHONPATH=/app/src:/app/tests ENV PYTHONUNBUFFERED=1 diff --git a/src/dashboard/app.py b/src/dashboard/app.py index f0d3eb16..d63c1da6 100644 --- a/src/dashboard/app.py +++ b/src/dashboard/app.py @@ -275,7 +275,11 @@ async def _task_processor_loop() -> None: def handle_thought(task): from timmy.thinking import thinking_engine try: - result = thinking_engine.think_once() + loop = asyncio.get_event_loop() + future = asyncio.run_coroutine_threadsafe( + thinking_engine.think_once(), loop + ) + result = future.result(timeout=120) return str(result) if result else "Thought completed" except Exception as e: logger.error("Thought processing failed: %s", e) @@ -457,15 +461,7 @@ async def lifespan(app: FastAPI): # Create all background tasks without waiting for them briefing_task = asyncio.create_task(_briefing_scheduler()) - # Register Timmy in swarm registry - from swarm import registry as swarm_registry - swarm_registry.register( - name="Timmy", - capabilities="chat,reasoning,research,planning", - agent_id="timmy", - ) - - # Run swarm recovery and log summary + # Run swarm recovery first (offlines all stale agents) from swarm.coordinator import coordinator as swarm_coordinator swarm_coordinator.initialize() rec = swarm_coordinator._recovery_summary @@ -476,6 +472,14 @@ async def lifespan(app: FastAPI): rec["agents_offlined"], ) + # Register Timmy AFTER recovery sweep so status sticks as "idle" + from swarm import registry as swarm_registry + swarm_registry.register( + name="Timmy", + capabilities="chat,reasoning,research,planning", + agent_id="timmy", + ) + # Spawn persona agents in background persona_task = asyncio.create_task(_spawn_persona_agents_background()) diff --git a/src/dashboard/routes/voice.py b/src/dashboard/routes/voice.py index d7ee407b..7a67f087 100644 --- a/src/dashboard/routes/voice.py +++ b/src/dashboard/routes/voice.py @@ -1,12 +1,16 @@ """Voice routes — /voice/* and /voice/enhanced/* endpoints. -Provides NLU intent detection, TTS control, and the full voice-to-action -pipeline (detect intent → execute → optionally speak). +Provides NLU intent detection, TTS control, the full voice-to-action +pipeline (detect intent → execute → optionally speak), and the voice +button UI page. """ import logging -from fastapi import APIRouter, Form +from fastapi import APIRouter, Form, Request +from fastapi.responses import HTMLResponse +from fastapi.templating import Jinja2Templates +from pathlib import Path from integrations.voice.nlu import detect_intent, extract_command from timmy.agent import create_timmy @@ -14,6 +18,7 @@ from timmy.agent import create_timmy logger = logging.getLogger(__name__) router = APIRouter(prefix="/voice", tags=["voice"]) +templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates")) @router.post("/nlu") @@ -56,6 +61,31 @@ async def tts_speak(text: str = Form(...)): return {"spoken": False, "reason": str(exc)} +# ── Voice button page ──────────────────────────────────────────────────── + + +@router.get("/button", response_class=HTMLResponse) +async def voice_button_page(request: Request): + """Render the voice button UI.""" + return templates.TemplateResponse(request, "voice_button.html") + + +@router.post("/command") +async def voice_command(text: str = Form(...)): + """Process a voice command (used by voice_button.html). + + Wraps the enhanced pipeline and returns the result in the format + the voice button template expects. + """ + result = await process_voice_input(text=text, speak_response=False) + return { + "command": { + "intent": result["intent"], + "response": result["response"] or result.get("error", "No response"), + } + } + + # ── Enhanced voice pipeline ────────────────────────────────────────────── @router.post("/enhanced/process") diff --git a/src/dashboard/templates/base.html b/src/dashboard/templates/base.html index d2f27fdb..35225083 100644 --- a/src/dashboard/templates/base.html +++ b/src/dashboard/templates/base.html @@ -9,13 +9,14 @@