From 781a365d0b75fdd9be2bce7b21258a66e0a8fb68 Mon Sep 17 00:00:00 2001 From: POWERFULMOVES Date: Tue, 3 Feb 2026 07:49:38 -0500 Subject: [PATCH] feat(archon): add persona service and API routes for agent creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Features - **PersonaService** with Supabase integration for persona fetching - `build_system_prompt()` for enhanced prompt generation with Archon templates - `create_agent_with_persona()` for Agent Zero integration ## API Endpoints - `GET /api/personas` - List all personas (with active_only filter) - `GET /api/personas/{id}` - Get persona details - `POST /api/personas/agent/create` - Create agent from persona - `GET /api/personas/thread-types` - Get thread type reference ## Changes - Added persona_service.py with async/sync blocking call fixes - Added persona_api.py with FastAPI routes - Updated dependabot.yml with correct directory paths - Updated .gitmodules with branch specification for docling - Added prometheus-client to all dependency group in pyproject.toml - Integrated persona routes into main.py 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/dependabot.yml | 10 +- .gitmodules | 1 + external/PMOVES-Agent-Zero | 2 +- external/PMOVES-BoTZ | 2 +- external/PMOVES-Deep-Serch | 2 +- external/PMOVES-HiRAG | 2 +- .../PMOVES-BotZ-gateway | 2 +- pmoves_multi_agent_pro_pack/PMOVES-tensorzero | 2 +- pmoves_multi_agent_pro_pack/docling | 2 +- python/pyproject.toml | 2 + python/src/server/api_routes/persona_api.py | 369 ++++++++++++++ python/src/server/main.py | 2 + python/src/server/services/persona_service.py | 457 ++++++++++++++++++ 13 files changed, 845 insertions(+), 10 deletions(-) create mode 100644 python/src/server/api_routes/persona_api.py create mode 100644 python/src/server/services/persona_service.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a5382e6502..ca0d3f041b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,19 +1,23 @@ version: 2 updates: + # Python dependencies in backend - package-ecosystem: "pip" - directory: "/" + directory: "/python" schedule: interval: "weekly" open-pull-requests-limit: 5 + # JavaScript/TypeScript dependencies in frontend UI - package-ecosystem: "npm" - directory: "/" + directory: "/archon-ui-main" schedule: interval: "weekly" open-pull-requests-limit: 5 + # Dockerfile in backend - package-ecosystem: "docker" - directory: "/" + directory: "/python" schedule: interval: "weekly" + # GitHub Actions workflows - package-ecosystem: "github-actions" directory: "/" schedule: diff --git a/.gitmodules b/.gitmodules index 24bbcf13aa..898b836e08 100644 --- a/.gitmodules +++ b/.gitmodules @@ -29,6 +29,7 @@ [submodule "pmoves_multi_agent_pro_pack/docling"] path = pmoves_multi_agent_pro_pack/docling url = https://github.com/POWERFULMOVES/docling.git + branch = main # Multi-Agent Pro Pack - MCP Gateway [submodule "pmoves_multi_agent_pro_pack/PMOVES-BotZ-gateway"] diff --git a/external/PMOVES-Agent-Zero b/external/PMOVES-Agent-Zero index 8642210ff1..d8eb4678a4 160000 --- a/external/PMOVES-Agent-Zero +++ b/external/PMOVES-Agent-Zero @@ -1 +1 @@ -Subproject commit 8642210ff15ecbfc0858819ac0bfe041f14df8fb +Subproject commit d8eb4678a4b3e50a5af5c32394c072dcac677801 diff --git a/external/PMOVES-BoTZ b/external/PMOVES-BoTZ index b39e3b4bf3..8461b77c37 160000 --- a/external/PMOVES-BoTZ +++ b/external/PMOVES-BoTZ @@ -1 +1 @@ -Subproject commit b39e3b4bf3974296e1d78f469ff1d0ae19ed551b +Subproject commit 8461b77c3768625da36d8379912db6e263610b45 diff --git a/external/PMOVES-Deep-Serch b/external/PMOVES-Deep-Serch index 41fa1d8cad..e2af6b6866 160000 --- a/external/PMOVES-Deep-Serch +++ b/external/PMOVES-Deep-Serch @@ -1 +1 @@ -Subproject commit 41fa1d8cad4ea8a778eb1c536d48f7e8e91d2552 +Subproject commit e2af6b6866e19eb300b954127f76adcddc7d1ae4 diff --git a/external/PMOVES-HiRAG b/external/PMOVES-HiRAG index d5bea168f7..9671dc17e6 160000 --- a/external/PMOVES-HiRAG +++ b/external/PMOVES-HiRAG @@ -1 +1 @@ -Subproject commit d5bea168f7192bbc13b9b88f34963f0464ad2eb9 +Subproject commit 9671dc17e633a7029a305c7ad67f41db02abd243 diff --git a/pmoves_multi_agent_pro_pack/PMOVES-BotZ-gateway b/pmoves_multi_agent_pro_pack/PMOVES-BotZ-gateway index b014856287..5dbe5d6f6b 160000 --- a/pmoves_multi_agent_pro_pack/PMOVES-BotZ-gateway +++ b/pmoves_multi_agent_pro_pack/PMOVES-BotZ-gateway @@ -1 +1 @@ -Subproject commit b0148562876389441f12c728d471683e567dee92 +Subproject commit 5dbe5d6f6bbad1d6b20c553fa1c7c94150a97228 diff --git a/pmoves_multi_agent_pro_pack/PMOVES-tensorzero b/pmoves_multi_agent_pro_pack/PMOVES-tensorzero index dcc5c5226a..6b1bc23f54 160000 --- a/pmoves_multi_agent_pro_pack/PMOVES-tensorzero +++ b/pmoves_multi_agent_pro_pack/PMOVES-tensorzero @@ -1 +1 @@ -Subproject commit dcc5c5226a05f2faaaf2b1c32a71f5313a099629 +Subproject commit 6b1bc23f540899764a9fcbc32ad85b4e24dbae47 diff --git a/pmoves_multi_agent_pro_pack/docling b/pmoves_multi_agent_pro_pack/docling index 06ae8ae29a..7f386587ed 160000 --- a/pmoves_multi_agent_pro_pack/docling +++ b/pmoves_multi_agent_pro_pack/docling @@ -1 +1 @@ -Subproject commit 06ae8ae29a5b8d9a2bf9aeecd7085d9f3093da2a +Subproject commit 7f386587ed9a28a839a928f3815d5ce1f3e05f8b diff --git a/python/pyproject.toml b/python/pyproject.toml index b0f9a880f2..10051438dd 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -137,6 +137,8 @@ all = [ # "docker>=6.1.0", "tldextract>=5.0.0", "logfire>=0.30.0", + # Metrics + "prometheus-client>=0.20.0", # MCP specific (mcp version) "mcp==1.12.2", # Agents specific diff --git a/python/src/server/api_routes/persona_api.py b/python/src/server/api_routes/persona_api.py new file mode 100644 index 0000000000..8ae3baca5a --- /dev/null +++ b/python/src/server/api_routes/persona_api.py @@ -0,0 +1,369 @@ +""" +Persona API endpoints for Archon + +This module provides HTTP endpoints for persona management and agent creation, +integrating with Agent Zero's persona-based agent system. + +Key features: +- List available personas with filtering +- Retrieve detailed persona information +- Create agents with persona-based behavior +- Reference data for thread types and persona metadata +""" + +from typing import Any + +from fastapi import APIRouter, HTTPException, Query +from pydantic import BaseModel, Field + +from ..config.logfire_config import get_logger +from ..services.persona_service import ( + AgentCreateResponse, + Persona, + PersonaCreateRequest, + PersonaService, + get_persona_service, +) + +logger = get_logger(__name__) + +router = APIRouter(prefix="/api/personas", tags=["personas"]) + + +# ============================================================================ +# Request/Response Models +# ============================================================================ + +class PersonaListResponse(BaseModel): + """Response model for persona list endpoint.""" + + personas: list[Persona] + total_count: int + + +class PersonaDetailResponse(BaseModel): + """Response model for persona detail endpoint.""" + + persona: Persona + + +class AgentCreateRequest(BaseModel): + """Request model for creating an agent from a persona.""" + + persona_id: str = Field(..., description="ID of persona to use for agent creation") + form_name: str | None = Field(None, description="Optional Archon form for behavior overrides") + overrides: dict[str, Any] | None = Field(None, description="Custom behavior weight overrides") + agent_name: str | None = Field(None, description="Custom name for the agent (defaults to persona name)") + + +class ThreadTypeResponse(BaseModel): + """Response model for thread types reference data.""" + + thread_types: list[dict[str, Any]] + description: str + + +# ============================================================================ +# API Endpoints +# ============================================================================ + +@router.get("", response_model=PersonaListResponse) +async def list_personas( + active_only: bool = Query( + True, + description="If True, only return active personas. If False, return all personas including inactive ones." + ) +): + """ + List all available personas from Supabase. + + Returns a list of personas with their system prompts, behavior weights, + and Archon-specific enhancements. Personas define AI agent personalities + and behavioral patterns. + + Args: + active_only: Filter to only return personas where is_active=True. + Defaults to True. Set to False to include inactive personas. + + Returns: + PersonaListResponse containing: + - personas: List of Persona objects + - total_count: Total number of personas returned + + Raises: + HTTPException 500: If persona retrieval fails + + Example: + GET /api/personas?active_only=true + + Response: + { + "personas": [ + { + "id": "code_reviewer", + "name": "Code Review Expert", + "description": "Expert code reviewer with focus on security...", + "system_prompt": "You are an expert code reviewer...", + "behavior_weights": {"creativity": 0.3, "formality": 0.9}, + "is_active": true, + "archon_enhancements": {...} + } + ], + "total_count": 1 + } + """ + try: + logger.info(f"Listing personas with active_only={active_only}") + + persona_service = get_persona_service() + success, result = await persona_service.list_personas(active_only=active_only) + + if not success: + error_msg = result.get("error", "Failed to list personas") + logger.error(f"Failed to list personas: {error_msg}") + raise HTTPException(status_code=500, detail=error_msg) + + logger.info(f"Successfully retrieved {result['total_count']} personas") + return PersonaListResponse( + personas=result["personas"], + total_count=result["total_count"] + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Unexpected error listing personas: {e}", exc_info=True) + raise HTTPException( + status_code=500, + detail=f"An unexpected error occurred while listing personas: {str(e)}" + ) + + +@router.get("/{persona_id}", response_model=PersonaDetailResponse) +async def get_persona(persona_id: str): + """ + Retrieve detailed information about a specific persona. + + Returns the complete persona definition including system prompt, + behavior weights, Archon enhancements, and metadata. + + Args: + persona_id: Unique identifier of the persona to retrieve + + Returns: + PersonaDetailResponse containing the requested Persona object + + Raises: + HTTPException 404: If persona with specified ID not found + HTTPException 500: If persona retrieval fails + + Example: + GET /api/personas/code_reviewer + + Response: + { + "persona": { + "id": "code_reviewer", + "name": "Code Review Expert", + "description": "Expert code reviewer...", + "system_prompt": "You are an expert code reviewer...", + "behavior_weights": {"creativity": 0.3, "formality": 0.9}, + "is_active": true, + "archon_enhancements": {...}, + "created_at": "2024-01-15T10:30:00Z", + "updated_at": "2024-01-15T10:30:00Z" + } + } + """ + try: + logger.info(f"Fetching persona: {persona_id}") + + persona_service = get_persona_service() + success, result = await persona_service.get_persona(persona_id) + + if not success: + error_msg = result.get("error", "Persona not found") + logger.warning(f"Failed to get persona {persona_id}: {error_msg}") + raise HTTPException(status_code=404, detail=error_msg) + + logger.info(f"Successfully retrieved persona: {result['persona'].name}") + return PersonaDetailResponse(persona=result["persona"]) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Unexpected error getting persona {persona_id}: {e}", exc_info=True) + raise HTTPException( + status_code=500, + detail=f"An unexpected error occurred while retrieving persona: {str(e)}" + ) + + +@router.post("/agent/create", response_model=AgentCreateResponse) +async def create_agent_from_persona(request: AgentCreateRequest): + """ + Create a new agent in Agent Zero using the specified persona. + + This endpoint: + 1. Fetches the persona from Supabase + 2. Enhances the system prompt with Archon-specific additions + 3. Applies form-based behavior weight overrides if specified + 4. Creates a new agent via Agent Zero's /api/persona/agent/create endpoint + + The created agent will exhibit behavior defined by the persona's system + prompt and behavior weights, with optional Archon form enhancements. + + Args: + request: AgentCreateRequest containing: + - persona_id: ID of persona to use (required) + - form_name: Optional Archon form for behavior overrides + - overrides: Optional dict of behavior weight overrides + - agent_name: Optional custom name for the agent + + Returns: + AgentCreateResponse containing: + - agent_id: ID of the created agent + - status: Agent status from Agent Zero + - message: Status message + - persona_id: ID of persona used + - system_prompt: Enhanced system prompt applied to agent + + Raises: + HTTPException 404: If specified persona not found + HTTPException 500: If agent creation fails + + Example: + POST /api/personas/agent/create + + Body: + { + "persona_id": "code_reviewer", + "form_name": "strict_review", + "overrides": {"creativity": 0.3, "formality": 0.9}, + "agent_name": "My Code Reviewer" + } + + Response: + { + "agent_id": "agent_abc123", + "status": "created", + "message": "Agent created successfully", + "persona_id": "code_reviewer", + "system_prompt": "You are an expert code reviewer..." + } + """ + try: + logger.info( + f"Creating agent from persona '{request.persona_id}' " + f"(form: {request.form_name or 'None'}, agent_name: {request.agent_name or 'None'})" + ) + + persona_service = get_persona_service() + success, result = await persona_service.create_agent_with_persona( + persona_id=request.persona_id, + form_name=request.form_name, + overrides=request.overrides, + agent_name=request.agent_name + ) + + if not success: + error_msg = result.get("error", "Failed to create agent") + logger.error(f"Failed to create agent from persona {request.persona_id}: {error_msg}") + raise HTTPException(status_code=500, detail=error_msg) + + logger.info(f"Successfully created agent {result['agent_id']} from persona {request.persona_id}") + return AgentCreateResponse(**result) + + except HTTPException: + raise + except Exception as e: + logger.error( + f"Unexpected error creating agent from persona {request.persona_id}: {e}", + exc_info=True + ) + raise HTTPException( + status_code=500, + detail=f"An unexpected error occurred while creating agent: {str(e)}" + ) + + +@router.get("/thread-types", response_model=ThreadTypeResponse) +async def get_thread_types(): + """ + Get reference data for available thread types. + + Thread types define the execution context and capabilities for agents. + This endpoint provides metadata about supported thread types for + agent creation and configuration. + + Returns: + ThreadTypeResponse containing: + - thread_types: List of thread type definitions + - description: Human-readable description of thread types + + Example: + GET /api/personas/thread-types + + Response: + { + "thread_types": [ + { + "id": "standard", + "name": "Standard Thread", + "description": "Default agent execution context", + "capabilities": ["chat", "tools", "memory"] + }, + { + "id": "research", + "name": "Research Thread", + "description": "Enhanced context for research tasks", + "capabilities": ["chat", "tools", "memory", "web_search", "knowledge_base"] + } + ], + "description": "Available thread types for agent creation" + } + """ + try: + logger.info("Fetching thread types reference data") + + # TODO: Integrate with Agent Zero's thread type API when available + # For now, return static reference data + thread_types = [ + { + "id": "standard", + "name": "Standard Thread", + "description": "Default agent execution context with core capabilities", + "capabilities": ["chat", "tools", "memory"], + "max_context_tokens": 8192, + "supported_models": ["gpt-4", "claude-3", "llama-3"] + }, + { + "id": "research", + "name": "Research Thread", + "description": "Enhanced context for research and knowledge-intensive tasks", + "capabilities": ["chat", "tools", "memory", "web_search", "knowledge_base", "citation"], + "max_context_tokens": 16384, + "supported_models": ["gpt-4", "claude-3"] + }, + { + "id": "creative", + "name": "Creative Thread", + "description": "Optimized for creative writing and content generation", + "capabilities": ["chat", "tools", "memory", "creative_mode"], + "max_context_tokens": 4096, + "supported_models": ["gpt-4", "claude-3", "llama-3"] + } + ] + + logger.info(f"Returning {len(thread_types)} thread types") + return ThreadTypeResponse( + thread_types=thread_types, + description="Available thread types for agent creation in Agent Zero" + ) + + except Exception as e: + logger.error(f"Unexpected error fetching thread types: {e}", exc_info=True) + raise HTTPException( + status_code=500, + detail=f"An unexpected error occurred while fetching thread types: {str(e)}" + ) diff --git a/python/src/server/main.py b/python/src/server/main.py index c1cbe93e96..45546d7302 100644 --- a/python/src/server/main.py +++ b/python/src/server/main.py @@ -28,6 +28,7 @@ from .api_routes.migration_api import router as migration_router from .api_routes.ollama_api import router as ollama_router from .api_routes.pages_api import router as pages_router +from .api_routes.persona_api import router as persona_router from .api_routes.progress_api import router as progress_router from .api_routes.projects_api import router as projects_router from .api_routes.providers_api import router as providers_router @@ -211,6 +212,7 @@ async def skip_health_check_logs(request, call_next): app.include_router(knowledge_router) app.include_router(pages_router) app.include_router(ollama_router) +app.include_router(persona_router) app.include_router(projects_router) app.include_router(progress_router) app.include_router(agent_chat_router) diff --git a/python/src/server/services/persona_service.py b/python/src/server/services/persona_service.py new file mode 100644 index 0000000000..94c61644c9 --- /dev/null +++ b/python/src/server/services/persona_service.py @@ -0,0 +1,457 @@ +""" +Persona Service for Archon + +This module provides persona management functionality, integrating with Supabase +for persona storage and Agent Zero for agent creation with persona-based behavior. + +Service follows Archon's established patterns: +- Async service methods with proper error handling +- Supabase client integration +- Service discovery for Agent Zero URL +- Comprehensive logging via unified config +- Pydantic models for type safety +""" + +from typing import Any + +import asyncio +import httpx +from pydantic import BaseModel, Field + +from ..config.logfire_config import get_logger +from ..config.service_discovery import get_agents_url +from ..utils import get_supabase_client + +logger = get_logger(__name__) + + +# ============================================================================ +# Pydantic Models +# ============================================================================ + +class Persona(BaseModel): + """ + Persona model representing an AI agent persona. + + Attributes: + id: Unique persona identifier + name: Display name for the persona + description: Human-readable description of persona characteristics + system_prompt: Base system prompt defining core behavior + behavior_weights: Dict of behavior modifiers (creativity, formality, etc.) + is_active: Whether persona is available for use + archon_enhancements: Optional Archon-specific prompt additions + created_at: Timestamp when persona was created + updated_at: Timestamp of last update + """ + + id: str + name: str + description: str + system_prompt: str + behavior_weights: dict[str, float] = Field(default_factory=dict) + is_active: bool = True + archon_enhancements: dict[str, str] | None = None + created_at: str | None = None + updated_at: str | None = None + + +class PersonaCreateRequest(BaseModel): + """Request model for creating a new agent with persona.""" + + persona_id: str = Field(..., description="ID of persona to use") + form_name: str | None = Field(None, description="Optional Archon form for behavior overrides") + overrides: dict[str, Any] | None = Field(None, description="Custom behavior weight overrides") + agent_name: str | None = Field(None, description="Custom name for the agent (defaults to persona name)") + + +class AgentCreateResponse(BaseModel): + """Response model from Agent Zero agent creation.""" + + agent_id: str + status: str + message: str + persona_id: str | None = None + system_prompt: str | None = None + + +# ============================================================================ +# Persona Service +# ============================================================================ + +class PersonaService: + """ + Service for managing AI agent personas and creating persona-based agents. + + This service integrates with: + - Supabase for persona storage and retrieval + - Agent Zero's /api/persona/agent/create endpoint for agent instantiation + - Archon's prompt templates for persona enhancement + """ + + def __init__(self, supabase_client=None): + """ + Initialize the PersonaService. + + Args: + supabase_client: Optional Supabase client instance. If not provided, + uses the global client from utils. + """ + self.supabase_client = supabase_client or get_supabase_client() + self.agent_zero_url = get_agents_url() + self.timeout = httpx.Timeout( + connect=5.0, + read=30.0, + write=10.0, + pool=5.0, + ) + + async def get_persona(self, persona_id: str) -> tuple[bool, dict[str, Any]]: + """ + Retrieve a specific persona by ID from Supabase. + + Args: + persona_id: Unique identifier of the persona to retrieve + + Returns: + Tuple of (success, result_dict) where result_dict contains: + - persona: Persona object on success + - error: Error message string on failure + + Example: + success, result = await persona_service.get_persona("dev_assistant") + if success: + persona = result["persona"] + print(f"Found persona: {persona.name}") + """ + try: + logger.info(f"Fetching persona: {persona_id}") + + # Run blocking Supabase call in thread pool to avoid blocking event loop + response = await asyncio.to_thread( + lambda: ( + self.supabase_client.table("archon_personas") + .select("*") + .eq("id", persona_id) + .execute() + ) + ) + + if not response.data: + logger.warning(f"Persona not found: {persona_id}") + return False, {"error": f"Persona with ID '{persona_id}' not found"} + + persona_data = response.data[0] + persona = Persona(**persona_data) + + logger.info(f"Successfully retrieved persona: {persona.name}") + return True, {"persona": persona} + + except Exception as e: + logger.error(f"Error fetching persona {persona_id}: {e}", exc_info=True) + return False, {"error": f"Failed to retrieve persona: {str(e)}"} + + async def list_personas(self, active_only: bool = True) -> tuple[bool, dict[str, Any]]: + """ + List all available personas from Supabase. + + Args: + active_only: If True, only return personas where is_active=True. + If False, return all personas including inactive ones. + + Returns: + Tuple of (success, result_dict) where result_dict contains: + - personas: List of Persona objects + - total_count: Total number of personas returned + - error: Error message string on failure + + Example: + success, result = await persona_service.list_personas(active_only=True) + if success: + for persona in result["personas"]: + print(f"{persona.name}: {persona.description}") + """ + try: + logger.info(f"Listing personas (active_only={active_only})") + + # Build query function to run in thread pool + def build_and_execute_query(): + query = self.supabase_client.table("archon_personas").select("*") + if active_only: + query = query.eq("is_active", True) + return query.order("name").execute() + + # Run blocking Supabase call in thread pool to avoid blocking event loop + response = await asyncio.to_thread(build_and_execute_query) + + personas = [Persona(**p) for p in response.data] + + logger.info(f"Retrieved {len(personas)} personas") + return True, { + "personas": personas, + "total_count": len(personas) + } + + except Exception as e: + logger.error(f"Error listing personas: {e}", exc_info=True) + return False, {"error": f"Failed to list personas: {str(e)}"} + + async def create_agent_with_persona( + self, + persona_id: str, + form_name: str | None = None, + overrides: dict[str, Any] | None = None, + agent_name: str | None = None + ) -> tuple[bool, dict[str, Any]]: + """ + Create a new agent in Agent Zero using the specified persona. + + This method: + 1. Fetches the persona from Supabase + 2. Enhances the system prompt with Archon-specific additions + 3. Applies form-based behavior weight overrides if specified + 4. Calls Agent Zero's /api/persona/agent/create endpoint + + Args: + persona_id: ID of persona to use for agent creation + form_name: Optional Archon form name for behavior overrides + overrides: Optional dict of behavior weight overrides (e.g., {"creativity": 0.8}) + agent_name: Optional custom name for the agent (defaults to persona name) + + Returns: + Tuple of (success, result_dict) where result_dict contains: + - agent_id: ID of created agent (on success) + - status: Agent status from Agent Zero + - message: Status message + - persona_id: ID of persona used + - system_prompt: Enhanced system prompt applied to agent + - error: Error message string on failure + + Example: + success, result = await persona_service.create_agent_with_persona( + persona_id="code_reviewer", + form_name="strict_review", + overrides={"creativity": 0.3, "formality": 0.9} + ) + if success: + print(f"Agent created: {result['agent_id']}") + """ + try: + # Step 1: Fetch persona from Supabase + persona_success, persona_result = await self.get_persona(persona_id) + if not persona_success: + return False, {"error": f"Failed to fetch persona: {persona_result.get('error')}"} + + persona: Persona = persona_result["persona"] + + # Step 2: Build enhanced system prompt + system_prompt = await self.build_system_prompt(persona, form_name) + + # Step 3: Apply behavior weight overrides + behavior_weights = persona.behavior_weights.copy() if persona.behavior_weights else {} + if overrides: + behavior_weights.update(overrides) + + # Step 4: Prepare agent creation request + request_data = { + "name": agent_name or persona.name, + "system_prompt": system_prompt, + "behavior_weights": behavior_weights, + "persona_id": persona_id, + "metadata": { + "form_name": form_name, + "archon_enhanced": True + } + } + + logger.info(f"Creating agent with persona '{persona.name}' at Agent Zero") + + # Step 5: Call Agent Zero API + endpoint = f"{self.agent_zero_url}/api/persona/agent/create" + + async with httpx.AsyncClient(timeout=self.timeout) as client: + response = await client.post( + endpoint, + json=request_data, + headers={"Content-Type": "application/json"} + ) + response.raise_for_status() + agent_response = response.json() + + logger.info(f"Agent created successfully: {agent_response.get('agent_id')}") + + return True, { + "agent_id": agent_response.get("agent_id"), + "status": agent_response.get("status", "created"), + "message": agent_response.get("message", "Agent created successfully"), + "persona_id": persona_id, + "system_prompt": system_prompt + } + + except httpx.HTTPStatusError as e: + logger.error(f"HTTP error creating agent: {e.response.status_code} - {e.response.text}") + return False, { + "error": f"Agent Zero returned error: {e.response.status_code}", + "details": e.response.text + } + except httpx.TimeoutException: + logger.error("Timeout creating agent in Agent Zero") + return False, {"error": "Request to Agent Zero timed out"} + except Exception as e: + logger.error(f"Error creating agent with persona: {e}", exc_info=True) + return False, {"error": f"Failed to create agent: {str(e)}"} + + async def build_system_prompt(self, persona: Persona, form_name: str | None = None) -> str: + """ + Build an enhanced system prompt from persona and optional Archon form. + + This method combines: + 1. Base persona system prompt + 2. Archon-specific prompt enhancements (if form_name provided) + 3. Behavior-based prompt adjustments + + Args: + persona: Persona object containing base system prompt + form_name: Optional Archon form name for additional enhancements + + Returns: + Enhanced system prompt string + + Example: + prompt = await persona_service.build_system_prompt(dev_persona, "code_review") + # Returns: "You are an expert developer... [base prompt] + # ### Archon Code Review Mode + # Focus on security, performance... [enhancements]" + """ + try: + # Start with base persona prompt + prompt_parts = [persona.system_prompt] + + # Add Archon-specific enhancements if form provided + if form_name: + archon_additions = await self.get_archon_prompt_enhancements(form_name) + if archon_additions: + prompt_parts.append(f"\n### Archon Enhancements ({form_name})") + prompt_parts.append(archon_additions) + + # Add behavior-based prompt adjustments + if persona.behavior_weights: + adjustments = self._build_behavior_adjustments(persona.behavior_weights) + if adjustments: + prompt_parts.append("\n### Behavioral Guidelines") + prompt_parts.append(adjustments) + + # Combine all parts + enhanced_prompt = "\n\n".join(prompt_parts) + + logger.debug(f"Built enhanced system prompt for persona '{persona.name}' " + f"(form: {form_name or 'None'})") + return enhanced_prompt + + except Exception as e: + logger.error(f"Error building system prompt: {e}", exc_info=True) + # Fallback to base prompt if enhancement fails + return persona.system_prompt + + async def get_archon_prompt_enhancements(self, form_name: str) -> str | None: + """ + Retrieve Archon-specific prompt enhancements for a given form. + + This method looks up form-specific prompt additions from the archon_prompts + table, allowing Archon to layer domain-specific behavior on top of base personas. + + Args: + form_name: Name of the Archon form to retrieve enhancements for + + Returns: + String of prompt enhancements or None if not found + + Example: + enhancements = await persona_service.get_archon_prompt_enhancements("code_review") + # Returns: "Focus on: security vulnerabilities, performance issues, + # code maintainability, and adherence to project standards." + """ + try: + if not form_name: + return None + + # Query archon_prompts table for form enhancements + # Run blocking Supabase call in thread pool to avoid blocking event loop + response = await asyncio.to_thread( + lambda: ( + self.supabase_client.table("archon_prompts") + .select("prompt") + .eq("prompt_name", f"form_{form_name}_enhancements") + .execute() + ) + ) + + if response.data: + enhancements = response.data[0].get("prompt", "") + logger.debug(f"Retrieved Archon enhancements for form: {form_name}") + return enhancements + else: + logger.debug(f"No Archon enhancements found for form: {form_name}") + return None + + except Exception as e: + logger.warning(f"Error retrieving Archon prompt enhancements for '{form_name}': {e}") + return None + + def _build_behavior_adjustments(self, behavior_weights: dict[str, float]) -> str: + """ + Build behavioral guideline text from behavior weight dict. + + Args: + behavior_weights: Dict of behavior names to weight values (0.0-1.0) + + Returns: + Formatted behavioral guideline string + """ + adjustments = [] + + for behavior, weight in behavior_weights.items(): + if weight >= 0.8: + level = "very high" + elif weight >= 0.6: + level = "high" + elif weight >= 0.4: + level = "moderate" + elif weight >= 0.2: + level = "low" + else: + level = "very low" + + behavior_human = behavior.replace("_", " ").title() + adjustments.append(f"- {behavior_human}: {level} priority") + + return "\n".join(adjustments) if adjustments else "" + + +# ============================================================================ +# Global Service Instance +# ============================================================================ + +_persona_service = None + + +def get_persona_service() -> PersonaService: + """ + Get or create the global PersonaService instance. + + Returns: + Global PersonaService singleton instance + """ + global _persona_service + if _persona_service is None: + _persona_service = PersonaService() + return _persona_service + + +__all__ = [ + "PersonaService", + "Persona", + "PersonaCreateRequest", + "AgentCreateResponse", + "get_persona_service", +]