Source code for haive.games.core.agent.player_agent

"""Configurable player agent system for games.

This module provides a flexible player agent abstraction that allows games to use
different LLM configurations for players without hardcoding them in engines.

The system supports:
- Dynamic LLM configuration per player
- Role-based agent configuration (player, analyzer, etc.)
- Easy swapping of LLMs and models
- Integration with the new LLM factory system

"""

from abc import ABC, abstractmethod
from typing import Any, Protocol

from haive.core.engine.aug_llm import AugLLMConfig
from haive.core.models.llm import LLMConfig
from haive.core.models.llm.base import (
    AnthropicLLMConfig,
    AzureLLMConfig,
    OpenAILLMConfig,
)
from pydantic import BaseModel, Field


[docs] def create_llm_config(model: str, **kwargs) -> LLMConfig: """Create an LLM config based on model string. This is a simple helper to create configs until a proper factory is available. """ # Simple provider detection based on model name if "gpt" in model.lower() or kwargs.get("model_provider") == "openai": return OpenAILLMConfig(model=model, **kwargs) elif "claude" in model.lower() or kwargs.get("model_provider") == "anthropic": return AnthropicLLMConfig(model=model, **kwargs) elif kwargs.get("model_provider") == "azure": return AzureLLMConfig(model=model, **kwargs) else: # Default to OpenAI return OpenAILLMConfig(model=model, **kwargs)
[docs] class PlayerRole(Protocol): """Protocol defining the interface for player roles."""
[docs] def get_role_name(self) -> str: """Get the name of this role.""" ...
[docs] def get_prompt_template(self) -> Any: """Get the prompt template for this role.""" ...
[docs] def get_structured_output_model(self) -> type | None: """Get the structured output model for this role.""" ...
[docs] class GamePlayerRole(BaseModel): """Standard implementation of a player role in a game. This class defines a specific role that a player can take in a game, such as 'white_player', 'black_analyzer', etc. """ role_name: str = Field(description="Name of the role (e.g., 'white_player')") prompt_template: Any = Field(description="Prompt template for this role") structured_output_model: type | None = Field( default=None, description="Expected output model" ) temperature: float | None = Field( default=None, description="Default temperature for this role" ) description: str = Field(default="", description="Description of this role") class Config: arbitrary_types_allowed = True
[docs] class PlayerAgentConfig(BaseModel): """Configuration for a player agent. This allows specifying which LLM configuration to use for a player without hardcoding it in the engine definitions. """ # LLM Configuration - can be string, LLMConfig, or dict llm_config: str | LLMConfig | dict[str, Any] = Field( description="LLM configuration - can be model string, LLMConfig instance, or config dict" ) # Optional overrides temperature: float | None = Field(default=None, description="Temperature override") model_provider: str | None = Field( default=None, description="Model provider override" ) # Player metadata player_name: str | None = Field( default=None, description="Human-readable player name" ) class Config: arbitrary_types_allowed = True
[docs] def create_llm_config(self) -> LLMConfig: """Create an LLMConfig instance from the configuration. Returns: LLMConfig: Configured LLM instance """ if isinstance(self.llm_config, LLMConfig): # Already an LLMConfig, use directly config = self.llm_config elif isinstance(self.llm_config, str): # Model string - use factory config = create_llm_config( self.llm_config, temperature=self.temperature, model_provider=self.model_provider, ) elif isinstance(self.llm_config, dict): # Dictionary config config_dict = self.llm_config.copy() if self.temperature is not None: config_dict["temperature"] = self.temperature if self.model_provider is not None: config_dict["model_provider"] = self.model_provider # Extract model for factory model = config_dict.pop("model", None) if not model: raise ValueError("Model must be specified in llm_config dict") config = create_llm_config(model, **config_dict) else: raise TypeError(f"Unsupported llm_config type: {type(self.llm_config)}") return config
[docs] class PlayerAgentFactory: """Factory for creating configurable player agents. This factory creates AugLLMConfig instances for game roles using configurable player agents instead of hardcoded LLM configurations. """
[docs] @staticmethod def create_player_engine( role: GamePlayerRole, agent_config: PlayerAgentConfig, **kwargs ) -> AugLLMConfig: """Create an AugLLMConfig for a player role. Args: role: The game role definition agent_config: The player agent configuration **kwargs: Additional parameters for AugLLMConfig Returns: AugLLMConfig: Configured engine for the role """ # Get LLM config from agent llm_config = agent_config.create_llm_config() # Use role temperature if agent doesn't specify one temperature = agent_config.temperature or role.temperature return AugLLMConfig( name=role.role_name, llm_config=llm_config, prompt_template=role.prompt_template, structured_output_model=role.structured_output_model, temperature=temperature, description=role.description, **kwargs, )
[docs] @staticmethod def create_engines_from_player_configs( roles: dict[str, GamePlayerRole], player_configs: dict[str, PlayerAgentConfig] ) -> dict[str, AugLLMConfig]: """Create a complete set of engines from role definitions and player configs. Args: roles: Dictionary of role name to role definition player_configs: Dictionary of role name to player agent config Returns: Dict[str, AugLLMConfig]: Dictionary of engines Examples: >>> roles = { ... "white_player": GamePlayerRole( ... role_name="white_player", ... prompt_template=white_move_prompt, ... structured_output_model=ChessPlayerDecision ... ) ... } >>> configs = { ... "white_player": PlayerAgentConfig(llm_config="gpt-4") ... } >>> engines = PlayerAgentFactory.create_engines_from_player_configs(roles, configs) """ engines = {} for role_name, role in roles.items(): if role_name in player_configs: agent_config = player_configs[role_name] engines[role_name] = PlayerAgentFactory.create_player_engine( role, agent_config ) else: raise ValueError(f"No player config provided for role: {role_name}") return engines
[docs] class ConfigurableGameAgent(ABC): """Abstract base for game agents with configurable players. This class provides the interface for game agents that support configurable player agents instead of hardcoded engines. """
[docs] @abstractmethod def get_role_definitions(self) -> dict[str, GamePlayerRole]: """Get the role definitions for this game. Returns: Dict[str, GamePlayerRole]: Dictionary of role name to role definition """
[docs] @abstractmethod def create_engines_from_player_configs( self, player_configs: dict[str, PlayerAgentConfig] ) -> dict[str, AugLLMConfig]: """Create engines from player configurations. Args: player_configs: Dictionary of role name to player agent config Returns: Dict[str, AugLLMConfig]: Dictionary of engines """ roles = self.get_role_definitions() return PlayerAgentFactory.create_engines_from_player_configs( roles, player_configs )
# Convenience functions for common configurations
[docs] def create_player_config( model: str | LLMConfig, temperature: float | None = None, player_name: str | None = None, **kwargs, ) -> PlayerAgentConfig: """Create a player agent configuration. Args: model: Model string, LLMConfig instance, or config dict temperature: Temperature setting player_name: Human-readable name for the player **kwargs: Additional configuration parameters Returns: PlayerAgentConfig: Configured player agent Examples: >>> config = create_player_config("gpt-4", temperature=0.7) >>> config = create_player_config("anthropic:claude-3-opus") >>> config = create_player_config({"model": "gpt-4", "provider": "openai"}) """ return PlayerAgentConfig( llm_config=model, temperature=temperature, player_name=player_name, **kwargs )
[docs] def create_simple_player_configs( white_model: str | LLMConfig = "gpt-4", black_model: str | LLMConfig = "claude-3-opus", temperature: float | None = None, **kwargs, ) -> dict[str, PlayerAgentConfig]: """Create simple player configurations for two-player games. Args: white_model: Model for white/first player black_model: Model for black/second player temperature: Temperature for both players **kwargs: Additional configuration parameters Returns: Dict[str, PlayerAgentConfig]: Player configurations Examples: >>> configs = create_simple_player_configs("gpt-4", "claude-3-opus", temperature=0.7) >>> # Creates configs for white_player, black_player, white_analyzer, black_analyzer """ base_config = {"temperature": temperature, **kwargs} return { "white_player": create_player_config( white_model, player_name="White", **base_config ), "black_player": create_player_config( black_model, player_name="Black", **base_config ), "white_analyzer": create_player_config( white_model, player_name="White Analyzer", **base_config ), "black_analyzer": create_player_config( black_model, player_name="Black Analyzer", **base_config ), }