Newer
Older
navi-1 / navi / profiles / loader.py
"""Auto-discover and load profiles from subdirectories.

A profile directory must contain:
  config.json      — metadata, model settings, enabled_tools
  system_prompt.txt — raw system prompt text

Any directory missing either file is silently skipped.
Errors in individual profiles are isolated and logged.
"""

import json
import structlog
from pathlib import Path

from .base import AgentProfile

log = structlog.get_logger()

_REQUIRED_CONFIG_KEYS = {"id", "name", "description", "enabled_tools"}


def _normalize_model(value: object) -> list[str]:
    """Accept str or list[str], always return list[str]."""
    if isinstance(value, list):
        return [str(m) for m in value] or ["gemma4:31b-cloud"]
    return [str(value)] if value else ["gemma4:31b-cloud"]


def load_profiles_from_dir(profiles_dir: str | Path) -> list[AgentProfile]:
    """Load all valid profile directories under profiles_dir."""
    base = Path(profiles_dir)
    profiles: list[AgentProfile] = []

    for entry in sorted(base.iterdir()):
        if not entry.is_dir():
            continue
        config_file = entry / "config.json"
        prompt_file = entry / "system_prompt.txt"
        if not config_file.exists() or not prompt_file.exists():
            continue

        try:
            config = json.loads(config_file.read_text(encoding="utf-8"))
            missing = _REQUIRED_CONFIG_KEYS - config.keys()
            if missing:
                log.warning(
                    "profile.loader.missing_keys",
                    profile_dir=entry.name,
                    missing=sorted(missing),
                )
                continue

            system_prompt = prompt_file.read_text(encoding="utf-8").strip()

            subagent_prompt_file = entry / "subagent_system_prompt.txt"
            subagent_system_prompt = (
                subagent_prompt_file.read_text(encoding="utf-8").strip()
                if subagent_prompt_file.exists()
                else ""
            )

            profiles.append(AgentProfile(
                id=config["id"],
                name=config["name"],
                description=config["description"],
                system_prompt=system_prompt,
                enabled_tools=config["enabled_tools"],
                llm_backend=config.get("llm_backend", "ollama"),
                model=_normalize_model(config.get("model", ["gemma4:31b-cloud"])),
                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", {}),
                subagent_tools=config.get("subagent_tools", []),
                subagent_planning_enabled=config.get("subagent_planning_enabled", False),
                subagent_system_prompt=subagent_system_prompt,
            ))
            log.debug("profile.loader.loaded", profile_id=config["id"])

        except Exception as exc:
            log.error("profile.loader.error", profile_dir=entry.name, error=str(exc))

    return profiles