Newer
Older
navi-1 / navi / api / routes / agents.py
"""Endpoints for listing available profiles and tools."""

from typing import Annotated

from fastapi import APIRouter, Depends, HTTPException

from navi.api.deps import get_current_user, get_mcp_manager, get_profile_registry, get_tool_registry
from navi.auth import User
from navi.config import settings
from navi.core import ProfileRegistry, ToolRegistry
from navi.mcp import McpManager, load_mcp_servers

router = APIRouter(prefix="/agents", tags=["agents"])


@router.get("/profiles")
async def list_profiles(
    profiles: Annotated[ProfileRegistry, Depends(get_profile_registry)],
    user: Annotated[User | None, Depends(get_current_user)] = None,
) -> list[dict]:
    is_admin = user is not None and user.role == "admin"
    result = []
    for p in profiles.all():
        if getattr(p, "is_admin_only", False) and not is_admin:
            continue
        result.append({
            "id": p.id,
            "name": p.name,
            "description": p.description,
            "enabled_tools": p.enabled_tools,
            "mcp_servers": p.mcp_servers,
            "llm_backend": p.llm_backend,
            "model": p.model,
            "temperature": p.temperature,
            "top_k": p.top_k,
            "top_p": p.top_p,
            "max_iterations": p.max_iterations,
            "iteration_budget_enabled": p.iteration_budget_enabled,
            "think_enabled": p.think_enabled,
            "subagent_think_enabled": p.subagent_think_enabled,
        })
    return result


def _resolve_mcp_tools(
    profile,
    mcp_manager: McpManager | None,
    tool_registry: ToolRegistry,
) -> list[str]:
    """Expand profile.mcp_servers into concrete tool names (mcp_server_tool)."""
    if not profile.mcp_servers or not mcp_manager:
        return []
    names: list[str] = []
    for server_name, groups in profile.mcp_servers.items():
        if "*" in groups:
            prefix = f"mcp_{server_name}_"
            for tool in tool_registry.all():
                if tool.name.startswith(prefix) and tool.name not in names:
                    names.append(tool.name)
        else:
            for group_name in groups:
                for tool_name in mcp_manager.resolve_group(server_name, group_name):
                    full_name = f"mcp_{server_name}_{tool_name}"
                    if full_name not in names:
                        names.append(full_name)
    return names


@router.get("/prompts")
async def list_system_prompts(
    profiles: Annotated[ProfileRegistry, Depends(get_profile_registry)],
    mcp_manager: Annotated[McpManager, Depends(get_mcp_manager)],
    tool_registry: Annotated[ToolRegistry, Depends(get_tool_registry)],
) -> list[dict]:
    """Return the full built system prompt for every profile, broken into sections.

    Mirrors Agent._build_system_prompt() exactly so the debug view shows
    what the model actually receives, including the dynamic profiles block.
    """
    all_profiles = profiles.all()
    persona = settings.navi_persona.strip()
    result = []

    for profile in all_profiles:
        sections = []

        if persona:
            sections.append({"label": "persona", "content": persona})

        sections.append({"label": "profile", "content": profile.system_prompt})

        other = [p for p in all_profiles if p.id != profile.id]
        if other:
            lines = [
                "## Available profiles",
                f"Current: **{profile.id}**",
            ]
            for p in other:
                desc = p.short_description or p.description
                lines.append(f"· {p.id}: {desc}")
            lines.append(
                "→ Switch profiles on your own judgment — do not ask for permission. "
                "When a task clearly fits another profile, call switch_profile immediately, "
                "then inform the user which profile is now active and why. "
                "Use list_profiles if you need details about a profile's capabilities."
            )
            sections.append({"label": "profiles block", "content": "\n".join(lines)})

        full = "\n\n---\n\n".join(s["content"] for s in sections)
        result.append({
            "profile_id": profile.id,
            "profile_name": profile.name,
            "model": profile.model,
            "enabled_tools": profile.enabled_tools,
            "mcp_servers": profile.mcp_servers,
            "resolved_mcp_tools": _resolve_mcp_tools(profile, mcp_manager, tool_registry),
            "sections": sections,
            "full": full,
            "total_chars": len(full),
        })

    return result


@router.get("/tools")
async def list_tools(
    tools: Annotated[ToolRegistry, Depends(get_tool_registry)],
) -> list[dict]:
    return [
        {
            "name": t.name,
            "description": t.description,
            "parameters": getattr(t, "parameters", {}),
        }
        for t in tools.all()
    ]


@router.get("/mcp_servers")
async def list_mcp_servers(
    mcp_manager: Annotated[McpManager, Depends(get_mcp_manager)],
    profiles: Annotated[ProfileRegistry, Depends(get_profile_registry)],
) -> list[dict]:
    """Return all configured MCP servers with groups, tools, instructions and profile mappings."""
    configs = load_mcp_servers(mcp_manager.config_path)
    connected = mcp_manager.clients
    all_profiles = profiles.all()

    result = []
    for name, cfg in configs.items():
        client = connected.get(name)
        server_tools = []
        if client and client.connected:
            try:
                tools = await client.list_tools()
                server_tools = [
                    {"name": t.name, "description": t.description or ""}
                    for t in tools
                ]
            except Exception:
                pass

        # Build profile mappings
        profile_refs = []
        for p in all_profiles:
            if name in (p.mcp_servers or {}):
                profile_refs.append({
                    "profile_id": p.id,
                    "profile_name": p.name,
                    "groups": p.mcp_servers[name],
                })

        # Merge instructions: server-provided + config overlay
        parts: list[str] = []
        if client and client.instructions:
            parts.append(client.instructions)
        if cfg.instructions:
            if parts:
                parts.append("")
            parts.append(cfg.instructions)

        result.append({
            "name": name,
            "connected": client is not None and client.connected,
            "transport": cfg.transport,
            "url": cfg.url,
            "command": cfg.command,
            "groups": cfg.groups,
            "instructions": "\n".join(parts) if parts else None,
            "tools": server_tools,
            "profiles": profile_refs,
        })

    return result