diff --git a/navi/core/agent.py b/navi/core/agent.py index 308f2ab..822c568 100644 --- a/navi/core/agent.py +++ b/navi/core/agent.py @@ -685,7 +685,7 @@ phase1_system = Message( role="system", content=( - self._build_system_prompt(profile.system_prompt) + self._build_system_prompt(profile) + "\n\n---\n\n" "[PLANNING — PHASE 1: ANALYSIS]\n\n" "Read the user's latest request.\n\n" @@ -728,7 +728,7 @@ phase2_system = Message( role="system", content=( - self._build_system_prompt(profile.system_prompt) + self._build_system_prompt(profile) + "\n\n---\n\n" "[PLANNING — PHASE 2: EXECUTION PLAN]\n\n" "Task analysis:\n\n" @@ -846,7 +846,7 @@ """ system_msg = Message( role="system", - content=self._build_system_prompt(profile.system_prompt), + content=self._build_system_prompt(profile), ) conv = [m for m in session_context if m.role != "system"] result: list[Message] = [system_msg] @@ -855,11 +855,30 @@ result.extend(conv) return result - def _build_system_prompt(self, profile_prompt: str) -> str: + def _build_system_prompt(self, profile: "AgentProfile") -> str: + parts: list[str] = [] + persona = settings.navi_persona.strip() if persona: - return f"{persona}\n\n---\n\n{profile_prompt}" - return profile_prompt + parts.append(persona) + + parts.append(profile.system_prompt) + + # Compact profiles block — every agent knows what other profiles exist + # and when to switch. Injected dynamically so new profiles appear automatically. + other = [p for p in self._profiles.all() 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("→ Use switch_profile to change profile. Use list_profiles for full details before switching.") + parts.append("\n".join(lines)) + + return "\n\n---\n\n".join(parts) def _tool_list(self, enabled: list[str]) -> list[Tool]: names = list(enabled) diff --git a/navi/core/registry.py b/navi/core/registry.py index 233cf58..2e6e308 100644 --- a/navi/core/registry.py +++ b/navi/core/registry.py @@ -12,6 +12,7 @@ FilesystemTool, HttpRequestTool, ImageViewTool, + ListProfilesTool, MemoryTool, SpawnAgentTool, SshExecTool, @@ -146,6 +147,9 @@ ) tools.register(switch_tool, builtin=True) + list_profiles_tool = ListProfilesTool(profile_registry=profiles) + tools.register(list_profiles_tool, builtin=True) + backends = BackendRegistry() backends.register( "ollama", diff --git a/navi/profiles/base.py b/navi/profiles/base.py index ebd8c9e..e97a348 100644 --- a/navi/profiles/base.py +++ b/navi/profiles/base.py @@ -19,3 +19,9 @@ max_iterations: int = 10 temperature: float = 0.7 planning_enabled: bool = False # if True, run a planning LLM call before the main loop + + # Profile discoverability — used for system prompt injection and list_profiles tool. + # short_description: 1-line summary shown in every system prompt to all profiles. + # full_description: structured dict with keys: specialization, when_to_use, key_tools. + short_description: str = "" + full_description: dict = field(default_factory=dict) diff --git a/navi/profiles/developer/config.json b/navi/profiles/developer/config.json index b6b825f..c45baa8 100644 --- a/navi/profiles/developer/config.json +++ b/navi/profiles/developer/config.json @@ -2,13 +2,19 @@ "id": "developer", "name": "Tool Developer", "description": "Write, test, and debug custom tools to extend Navi's capabilities.", + "short_description": "Writing, testing, and debugging Navi's own Python tools.", + "full_description": { + "specialization": "Writing new Python tools that extend Navi's capabilities, debugging and fixing existing tools, hot-reloading the tool registry, and testing tool behavior. Full access to tools directory, test runner, and reload mechanism.", + "when_to_use": "When the user asks to create a new tool, modify an existing tool, fix a broken tool, or test tool functionality. Not for general software development tasks — use secretary for those.", + "key_tools": "write_tool, reload_tools, delete_tool, test_tool, filesystem, terminal, code_exec, memory" + }, "llm_backend": "ollama", "model": "gemma4:26b-a4b-it-q4_K_M", "temperature": 0.2, "max_iterations": 40, "planning_enabled": true, "enabled_tools": [ - "todo", "scratchpad", "switch_profile", + "todo", "scratchpad", "switch_profile", "list_profiles", "web_search", "web_view", "http_request", "filesystem", "code_exec", "terminal", "image_view", "memory", @@ -18,4 +24,4 @@ "share_file", "email_manager" ] -} \ No newline at end of file +} diff --git a/navi/profiles/loader.py b/navi/profiles/loader.py index 0949fcd..6eb0bc5 100644 --- a/navi/profiles/loader.py +++ b/navi/profiles/loader.py @@ -56,6 +56,8 @@ temperature=config.get("temperature", 0.7), max_iterations=config.get("max_iterations", 20), planning_enabled=config.get("planning_enabled", False), + short_description=config.get("short_description", ""), + full_description=config.get("full_description", {}), )) log.debug("profile.loader.loaded", profile_id=config["id"]) diff --git a/navi/profiles/secretary/config.json b/navi/profiles/secretary/config.json index 5acc51e..be6ed05 100644 --- a/navi/profiles/secretary/config.json +++ b/navi/profiles/secretary/config.json @@ -2,13 +2,19 @@ "id": "secretary", "name": "Personal Secretary", "description": "General-purpose assistant for research, writing, and everyday tasks.", + "short_description": "Research, writing, analysis, email, and everyday personal tasks.", + "full_description": { + "specialization": "General-purpose personal assistant. Web research, document writing, data analysis, email correspondence, planning, calculations, and any everyday task that doesn't require direct server access or tool development.", + "when_to_use": "Default profile for most requests. If you're unsure which profile to use, this one is correct. Switch away only when the task clearly requires server/infrastructure access (server_admin) or modifying Navi's own tools (developer).", + "key_tools": "web_search, web_view, filesystem, code_exec, gmail, todo, scratchpad, spawn_agent, memory" + }, "llm_backend": "ollama", "model": "gemma4:26b-a4b-it-q4_K_M", "temperature": 0.7, "max_iterations": 40, "planning_enabled": true, "enabled_tools": [ - "todo", "scratchpad", "switch_profile", + "todo", "scratchpad", "switch_profile", "list_profiles", "web_search", "web_view", "http_request", "filesystem", "code_exec", "image_view", "memory", @@ -18,4 +24,4 @@ "weather", "email_manager" ] -} \ No newline at end of file +} diff --git a/navi/profiles/server_admin/config.json b/navi/profiles/server_admin/config.json index fa4d21e..fa47a59 100644 --- a/navi/profiles/server_admin/config.json +++ b/navi/profiles/server_admin/config.json @@ -2,13 +2,19 @@ "id": "server_admin", "name": "Server Administrator", "description": "Server administration, monitoring, and infrastructure tasks.", + "short_description": "Linux server administration, SSH operations, monitoring, infrastructure.", + "full_description": { + "specialization": "Remote server operations via SSH, system diagnostics, service management, log analysis, network troubleshooting, process monitoring, and infrastructure automation.", + "when_to_use": "When the task involves SSH access to servers, running system commands, managing Linux services, analyzing logs, monitoring resources, or any hands-on infrastructure work.", + "key_tools": "ssh_exec, terminal, filesystem, code_exec, web_search, spawn_agent, memory" + }, "llm_backend": "ollama", "model": "gemma4:26b-a4b-it-q4_K_M", "temperature": 0.2, "max_iterations": 40, "planning_enabled": true, "enabled_tools": [ - "todo", "scratchpad", "switch_profile", + "todo", "scratchpad", "switch_profile", "list_profiles", "web_search", "web_view", "http_request", "filesystem", "code_exec", "terminal", "ssh_exec", "image_view", "memory", @@ -17,4 +23,4 @@ "share_file", "email_manager" ] -} \ No newline at end of file +} diff --git a/navi/tools/__init__.py b/navi/tools/__init__.py index 64101a2..d6a0a8f 100644 --- a/navi/tools/__init__.py +++ b/navi/tools/__init__.py @@ -12,6 +12,7 @@ from .todo import TodoTool from .scratchpad import ScratchpadTool from .switch_profile import SwitchProfileTool +from .list_profiles import ListProfilesTool from .web_search import WebSearchTool from .web_view import WebViewTool @@ -33,4 +34,5 @@ "TodoTool", "ScratchpadTool", "SwitchProfileTool", + "ListProfilesTool", ] diff --git a/navi/tools/list_profiles.py b/navi/tools/list_profiles.py new file mode 100644 index 0000000..0ca5669 --- /dev/null +++ b/navi/tools/list_profiles.py @@ -0,0 +1,59 @@ +"""Built-in tool: list available agent profiles with structured descriptions.""" + +from navi.tools.base import Tool, ToolResult + + +class ListProfilesTool(Tool): + name = "list_profiles" + description = ( + "List all available agent profiles with their specialization, use cases, and key tools. " + "Call this before switch_profile when you need details about what a profile does " + "or to confirm which profile best fits the current task." + ) + parameters = { + "type": "object", + "properties": { + "profile_id": { + "type": "string", + "description": "Optional. Return full details for one specific profile only.", + } + }, + "required": [], + } + + def __init__(self, profile_registry) -> None: + self._profiles = profile_registry + + async def execute(self, params: dict) -> ToolResult: + profile_id = (params.get("profile_id") or "").strip() + + if profile_id: + try: + p = self._profiles.get(profile_id) + except Exception: + available = ", ".join(x.id for x in self._profiles.all()) + return ToolResult( + success=False, output="", + error=f"Profile '{profile_id}' not found. Available: {available}", + ) + return ToolResult(success=True, output=self._format(p)) + + sections = [self._format(p) for p in self._profiles.all()] + return ToolResult(success=True, output="\n\n".join(sections)) + + @staticmethod + def _format(p) -> str: + fd = p.full_description or {} + lines = [f"## {p.name} [{p.id}]"] + if p.short_description: + lines.append(p.short_description) + if fd.get("specialization"): + lines.append(f"\nSpecialization: {fd['specialization']}") + if fd.get("when_to_use"): + lines.append(f"When to use: {fd['when_to_use']}") + if fd.get("key_tools"): + tools = fd["key_tools"] + if isinstance(tools, list): + tools = ", ".join(tools) + lines.append(f"Key tools: {tools}") + return "\n".join(lines)