Files
zeekay 4ebf26df3e feat(mcp): unify tool surfaces, rename iching→zen, fix test infrastructure
- Consolidate memory tools into single unified surface
- Rename IChingTool → ZenTool across agent tools
- Rename tool files: proc_tool→exec_tool, net_tool→fetch_tool,
  vcs_tool→git_tool, todo_tool→tasks_tool
- Add hanzo_tool.py for unified Hanzo API surface
- Add workspace_tool.py for config management
- Add hanzo-tools-ui package with GitHub API integration
- Add unified.py core tool dispatcher
- Update IAM client to RFC-compliant OAuth endpoints
- Fix pytest-asyncio plugin conflict: pin <1.0.0, fix addopts
  double-registration, and align test APIs to current signatures
- Fix integration tests: PermissionManager API, ReviewProtocol
  focus values, AgentTool parameter names, mock llm module for CI
2026-03-06 03:13:54 -08:00

233 lines
7.8 KiB
Python

"""Adapter to load personalities from hanzo-persona package.
This module bridges the hanzo-persona package (which has rich persona profiles)
with the hanzo-mcp tool personality system.
"""
import json
import logging
from pathlib import Path
from typing import Any, Dict, List, Optional
from hanzo_mcp.tools.common.personality import (
AI_TOOLS,
BUILD_TOOLS,
DATABASE_TOOLS,
ESSENTIAL_TOOLS,
SEARCH_TOOLS,
UNIX_TOOLS,
VECTOR_TOOLS,
PersonalityRegistry,
ToolPersonality,
)
logger = logging.getLogger(__name__)
# Tool mappings for persona categories
CATEGORY_TOOL_MAPPINGS = {
"programmer": ESSENTIAL_TOOLS
+ ["symbols", "multi_edit"]
+ SEARCH_TOOLS
+ BUILD_TOOLS,
"scientist": ESSENTIAL_TOOLS + ["jupyter", "critic"] + AI_TOOLS + VECTOR_TOOLS,
"philosopher": ESSENTIAL_TOOLS + ["critic", "think", "search"],
"artist": ESSENTIAL_TOOLS + ["watch", "jupyter"] + AI_TOOLS,
"leader": ESSENTIAL_TOOLS + ["tasks", "rules", "agent"] + AI_TOOLS,
"writer": ESSENTIAL_TOOLS + ["search", "critic", "tasks"],
"mathematician": ESSENTIAL_TOOLS + ["jupyter", "symbols", "critic"],
"musician": ESSENTIAL_TOOLS + ["watch", "tasks"],
"athlete": ESSENTIAL_TOOLS + ["tasks", "watch", "process"],
"entrepreneur": ESSENTIAL_TOOLS
+ ["tasks", "agent", "consensus"]
+ BUILD_TOOLS
+ DATABASE_TOOLS,
"activist": ESSENTIAL_TOOLS + ["search", "tasks", "rules"],
"religious_leader": ESSENTIAL_TOOLS + ["search", "critic", "think"],
"military_leader": ESSENTIAL_TOOLS + ["tasks", "process", "critic"] + UNIX_TOOLS,
"explorer": ESSENTIAL_TOOLS + ["search", "watch", "tasks"],
"comedian": ESSENTIAL_TOOLS + ["tasks", "watch", "critic"],
"default": ESSENTIAL_TOOLS + AI_TOOLS,
}
def persona_to_tool_personality(persona: Dict[str, Any]) -> Optional[ToolPersonality]:
"""Convert a hanzo-persona profile to a ToolPersonality.
Args:
persona: Raw persona dict from hanzo-persona package
Returns:
ToolPersonality instance or None if invalid
"""
try:
# Get name - could be 'id' or 'name' field
name = persona.get("id") or persona.get("name", "").lower().replace(" ", "_")
if not name:
return None
# Get programmer name (display name)
programmer = persona.get("programmer") or persona.get("name", name)
# Get description
description = persona.get("description", "")
# Get philosophy
philosophy = persona.get("philosophy", "")
# Determine tools based on category and persona's tool preferences
category = persona.get("category", "default")
base_tools = CATEGORY_TOOL_MAPPINGS.get(
category, CATEGORY_TOOL_MAPPINGS["default"]
)
# Add tools from persona's tool preferences
tools_config = persona.get("tools", {})
if isinstance(tools_config, dict):
essential = tools_config.get("essential", [])
preferred = tools_config.get("preferred", [])
all_tools = list(set(base_tools + essential + preferred))
elif isinstance(tools_config, list):
all_tools = list(set(base_tools + tools_config))
else:
all_tools = list(base_tools)
# Map common tool names to hanzo-mcp tool names
tool_name_map = {
"python": "uvx",
"formatter": "edit",
"testing": "bash",
"documentation": "rules",
"pytest": "bash",
}
all_tools = [tool_name_map.get(t, t) for t in all_tools]
all_tools = list(set(all_tools)) # Dedupe
# Get environment variables
environment = persona.get("environment", {})
# Extract extended fields
ocean = persona.get("ocean")
behavioral_traits = persona.get("behavioral_traits")
cognitive_style = persona.get("cognitive_style")
social_dynamics = persona.get("social_dynamics")
communication_patterns = persona.get("communication_patterns")
work_methodology = persona.get("work_methodology")
emotional_profile = persona.get("emotional_profile")
return ToolPersonality(
name=name,
programmer=programmer,
description=description,
tools=all_tools,
environment=environment if environment else None,
philosophy=philosophy if philosophy else None,
category=category,
ocean=ocean,
behavioral_traits=behavioral_traits,
cognitive_style=cognitive_style,
social_dynamics=social_dynamics,
communication_patterns=communication_patterns,
work_methodology=work_methodology,
emotional_profile=emotional_profile,
)
except Exception as e:
logger.warning(f"Failed to convert persona: {e}")
return None
def load_personas_from_package() -> int:
"""Load all personas from hanzo-persona package.
Returns:
Number of personas loaded
"""
loaded_count = 0
try:
# Try to import hanzo-persona package
from personalities.personality_loader import PERSONA_DIR, PersonalityLoader
# Load from all_personalities.json if exists
all_personalities_file = PERSONA_DIR / "all_personalities.json"
if all_personalities_file.exists():
loader = PersonalityLoader(all_personalities_file)
for persona in loader.get_all():
tp = persona_to_tool_personality(persona)
if tp:
PersonalityRegistry.register(tp)
loaded_count += 1
logger.info(f"Loaded {loaded_count} personas from all_personalities.json")
return loaded_count
except ImportError:
pass
except Exception as e:
logger.debug(f"Could not load from personalities module: {e}")
# Try loading from profiles directory
try:
profiles_dir = _find_profiles_dir()
if profiles_dir and profiles_dir.exists():
loaded_count = _load_from_profiles_dir(profiles_dir)
logger.info(f"Loaded {loaded_count} personas from profiles directory")
return loaded_count
except Exception as e:
logger.debug(f"Could not load from profiles dir: {e}")
return loaded_count
def _find_profiles_dir() -> Optional[Path]:
"""Find the hanzo-persona profiles directory."""
# Check common locations
search_paths = [
Path.home() / "work" / "hanzo" / "experiments" / "persona" / "profiles",
Path.home() / "work" / "hanzo" / "persona" / "profiles",
Path("/opt/hanzo/persona/profiles"),
]
# Check HANZO_PERSONA_DIR environment variable
import os
env_path = os.environ.get("HANZO_PERSONA_DIR")
if env_path:
search_paths.insert(0, Path(env_path) / "profiles")
for path in search_paths:
if path.exists():
return path
return None
def _load_from_profiles_dir(profiles_dir: Path) -> int:
"""Load personas from individual JSON files in profiles directory."""
loaded_count = 0
for json_file in profiles_dir.glob("*.json"):
if json_file.name in ["index.json", "categories.json"]:
continue
try:
with open(json_file, "r", encoding="utf-8") as f:
persona = json.load(f)
tp = persona_to_tool_personality(persona)
if tp:
PersonalityRegistry.register(tp)
loaded_count += 1
except Exception as e:
logger.debug(f"Failed to load {json_file}: {e}")
return loaded_count
def get_persona_categories() -> List[str]:
"""Get list of available persona categories."""
return list(CATEGORY_TOOL_MAPPINGS.keys())
def get_personas_by_category(category: str) -> List[ToolPersonality]:
"""Get all personas in a specific category."""
return [p for p in PersonalityRegistry.list() if p.category == category]