#2869 - Commit before switching branches. Changes to make pydantic happy with AgentLog

This commit is contained in:
Charlie Crane
2024-12-05 14:00:44 +00:00
parent 917386d638
commit 1798ec6fe0
9 changed files with 54 additions and 33 deletions

View File

@@ -1,8 +1,11 @@
# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK
import logging import logging
from abc import ABC
from pathlib import Path from pathlib import Path
from typing import Optional
from prettytable import MARKDOWN, PrettyTable from prettytable import MARKDOWN, PrettyTable
from pydantic import BaseModel
from primaite.simulator import LogLevel, SIM_OUTPUT from primaite.simulator import LogLevel, SIM_OUTPUT
@@ -18,22 +21,27 @@ class _NotJSONFilter(logging.Filter):
return not record.getMessage().startswith("{") and not record.getMessage().endswith("}") return not record.getMessage().startswith("{") and not record.getMessage().endswith("}")
class AgentLog: class AgentLog(BaseModel):
""" """
A Agent Log class is a simple logger dedicated to managing and writing logging updates and information for an agent. A Agent Log class is a simple logger dedicated to managing and writing logging updates and information for an agent.
Each log message is written to a file located at: <simulation output directory>/agent_name/agent_name.log Each log message is written to a file located at: <simulation output directory>/agent_name/agent_name.log
""" """
def __init__(self, agent_name: str): agent_name: str = "unnamed_agent"
current_episode: int = 1
current_timestep: int = 0
def __init__(self, agent_name: Optional[str]):
""" """
Constructs a Agent Log instance for a given hostname. Constructs a Agent Log instance for a given hostname.
:param hostname: The hostname associated with the system logs being recorded. :param hostname: The hostname associated with the system logs being recorded.
""" """
self.agent_name = agent_name super().__init__()
self.current_episode: int = 1 self.agent_name = agent_name or "unnamed_agent"
self.current_timestep: int = 0 # self.current_episode: int = 1
# self.current_timestep: int = 0
self.setup_logger() self.setup_logger()
@property @property

View File

@@ -97,6 +97,7 @@ class AbstractAgent(BaseModel, ABC, identifier="Abstract_Agent"):
_registry: ClassVar[Dict[str, Type[AbstractAgent]]] = {} _registry: ClassVar[Dict[str, Type[AbstractAgent]]] = {}
config: "AbstractAgent.ConfigSchema" config: "AbstractAgent.ConfigSchema"
agent_name = "Abstract_Agent"
class ConfigSchema(BaseModel): class ConfigSchema(BaseModel):
""" """
@@ -115,13 +116,13 @@ class AbstractAgent(BaseModel, ABC, identifier="Abstract_Agent"):
""" """
type: str type: str
agent_name: ClassVar[str] agent_name: str = "Abstact_Agent"
logger: AgentLog = AgentLog(agent_name) logger: AgentLog = AgentLog(agent_name)
history: List[AgentHistoryItem] = [] history: List[AgentHistoryItem] = []
action_manager: Optional[ActionManager] = None action_manager: Optional[ActionManager] = None
observation_manager: Optional[ObservationManager] = None observation_manager: Optional[ObservationManager] = None
reward_function: Optional[RewardFunction] = None reward_function: Optional[RewardFunction] = None
agent_settings = Optional[AgentSettings] = None agent_settings: Optional[AgentSettings] = None
def __init_subclass__(cls, identifier: str, **kwargs: Any) -> None: def __init_subclass__(cls, identifier: str, **kwargs: Any) -> None:
super().__init_subclass__(**kwargs) super().__init_subclass__(**kwargs)
@@ -213,6 +214,11 @@ class AbstractAgent(BaseModel, ABC, identifier="Abstract_Agent"):
class AbstractScriptedAgent(AbstractAgent, identifier="Abstract_Scripted_Agent"): class AbstractScriptedAgent(AbstractAgent, identifier="Abstract_Scripted_Agent"):
"""Base class for actors which generate their own behaviour.""" """Base class for actors which generate their own behaviour."""
class ConfigSchema(AbstractAgent.ConfigSchema):
"""Configuration Schema for AbstractScriptedAgents."""
agent_name: str = "Abstract_Scripted_Agent"
@abstractmethod @abstractmethod
def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]:
"""Return an action to be taken in the environment.""" """Return an action to be taken in the environment."""
@@ -227,6 +233,7 @@ class ProxyAgent(AbstractAgent, identifier="Proxy_Agent"):
class ConfigSchema(AbstractAgent.ConfigSchema): class ConfigSchema(AbstractAgent.ConfigSchema):
"""Configuration Schema for Proxy Agent.""" """Configuration Schema for Proxy Agent."""
agent_name: str = "Proxy_Agent"
agent_settings = Union[AgentSettings | None] = None agent_settings = Union[AgentSettings | None] = None
most_reason_action: ActType most_reason_action: ActType
flatten_obs: bool = agent_settings.flatten_obs if agent_settings else False flatten_obs: bool = agent_settings.flatten_obs if agent_settings else False

View File

@@ -15,6 +15,7 @@ class AbstractTAPAgent(AbstractScriptedAgent, identifier="Abstract_TAP"):
class ConfigSchema(AbstractScriptedAgent.ConfigSchema): class ConfigSchema(AbstractScriptedAgent.ConfigSchema):
"""Configuration schema for Abstract TAP agents.""" """Configuration schema for Abstract TAP agents."""
agent_name: str = "Abstract_TAP"
starting_node_name: str starting_node_name: str
next_execution_timestep: int next_execution_timestep: int
@@ -32,3 +33,11 @@ class AbstractTAPAgent(AbstractScriptedAgent, identifier="Abstract_TAP"):
-self.config.agent_settings.start_settings.variance, self.config.agent_settings.start_settings.variance -self.config.agent_settings.start_settings.variance, self.config.agent_settings.start_settings.variance
) )
self.config.next_execution_timestep = timestep + random_timestep_increment self.config.next_execution_timestep = timestep + random_timestep_increment
def _select_start_node(self) -> None:
"""Set the starting starting node of the agent to be a random node from this agent's action manager."""
# we are assuming that every node in the node manager has a data manipulation application at idx 0
num_nodes = len(self.config.action_manager.node_names)
starting_node_idx = random.randint(0, num_nodes - 1)
self.starting_node_name = self.config.action_manager.node_names[starting_node_idx]
self.config.logger.debug(f"Selected Starting node ID: {self.starting_node_name}")

View File

@@ -16,6 +16,11 @@ class DataManipulationAgent(AbstractTAPAgent, identifier="Data_Manipulation_Agen
"""Configuration Schema for DataManipulationAgent.""" """Configuration Schema for DataManipulationAgent."""
starting_application_name: str starting_application_name: str
agent_name: str = "Data_Manipulation_Agent"
def __init__(self) -> None:
"""Meh."""
self.setup_agent()
@property @property
def next_execution_timestep(self) -> int: def next_execution_timestep(self) -> int:
@@ -52,10 +57,3 @@ class DataManipulationAgent(AbstractTAPAgent, identifier="Data_Manipulation_Agen
"""Set the next execution timestep when the episode resets.""" """Set the next execution timestep when the episode resets."""
self._select_start_node() self._select_start_node()
self._set_next_execution_timestep(self.config.agent_settings.start_settings.start_step) self._set_next_execution_timestep(self.config.agent_settings.start_settings.start_step)
def _select_start_node(self) -> None:
"""Set the starting starting node of the agent to be a random node from this agent's action manager."""
# we are assuming that every node in the node manager has a data manipulation application at idx 0
num_nodes = len(self.action_manager.node_names)
self.starting_node_idx = random.randint(0, num_nodes - 1)
self.config.logger.debug(msg=f"Select Start Node ID: {self.starting_node_idx}")

View File

@@ -18,6 +18,7 @@ class ProbabilisticAgent(AbstractScriptedAgent, identifier="Probabilistic_Agent"
class ConfigSchema(pydantic.BaseModel): class ConfigSchema(pydantic.BaseModel):
"""Config schema for Probabilistic agent settings.""" """Config schema for Probabilistic agent settings."""
agent_name: str = "Probabilistic_Agent"
model_config = pydantic.ConfigDict(extra="forbid") model_config = pydantic.ConfigDict(extra="forbid")
"""Strict validation.""" """Strict validation."""
action_probabilities: Dict[int, float] action_probabilities: Dict[int, float]

View File

@@ -10,7 +10,12 @@ from primaite.game.agent.interface import AbstractScriptedAgent
class RandomAgent(AbstractScriptedAgent, identifier="Random_Agent"): class RandomAgent(AbstractScriptedAgent, identifier="Random_Agent"):
"""Agent that ignores its observation and acts completely at random.""" """Agent that ignores its observation and acts completely at random."""
def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: class ConfigSchema(AbstractScriptedAgent.ConfigSchema):
"""Configuration Schema for Random Agents."""
agent_name = "Random_Agent"
def get_action(self) -> Tuple[str, Dict]:
"""Sample the action space randomly. """Sample the action space randomly.
:param obs: Current observation for this agent, not used in RandomAgent :param obs: Current observation for this agent, not used in RandomAgent
@@ -31,6 +36,8 @@ class PeriodicAgent(AbstractScriptedAgent, identifier="Periodic_Agent"):
class ConfigSchema(AbstractScriptedAgent.ConfigSchema): class ConfigSchema(AbstractScriptedAgent.ConfigSchema):
"""Configuration Schema for Periodic Agent.""" """Configuration Schema for Periodic Agent."""
agent_name = "Periodic_Agent"
"""Name of the agent."""
start_step: int = 20 start_step: int = 20
"The timestep at which an agent begins performing it's actions." "The timestep at which an agent begins performing it's actions."
start_variance: int = 5 start_variance: int = 5
@@ -69,9 +76,9 @@ class PeriodicAgent(AbstractScriptedAgent, identifier="Periodic_Agent"):
def get_action(self, obs: ObsType, timestep: int) -> Tuple[str, Dict]: def get_action(self, obs: ObsType, timestep: int) -> Tuple[str, Dict]:
"""Do nothing, unless the current timestep is the next execution timestep, in which case do the action.""" """Do nothing, unless the current timestep is the next execution timestep, in which case do the action."""
if timestep == self.next_execution_timestep and self.num_executions < self.settings.max_executions: if timestep == self.next_execution_timestep and self.num_executions < self.config.max_executions:
self.num_executions += 1 self.num_executions += 1
self._set_next_execution_timestep(timestep + self.settings.frequency, self.settings.variance) self._set_next_execution_timestep(timestep + self.config.frequency, self.config.variance)
return "NODE_APPLICATION_EXECUTE", {"node_id": 0, "application_id": 0} return "NODE_APPLICATION_EXECUTE", {"node_id": 0, "application_id": 0}
return "DONOTHING", {} return "DONOTHING", {}

View File

@@ -19,20 +19,19 @@ class TAP001(AbstractTAPAgent, identifier="TAP001"):
class ConfigSchema(AbstractTAPAgent.ConfigSchema): class ConfigSchema(AbstractTAPAgent.ConfigSchema):
"""Configuration Schema for TAP001 Agent.""" """Configuration Schema for TAP001 Agent."""
agent_name: str = "TAP001"
installed: bool = False installed: bool = False
def __init__(self) -> None:
"""___init___ bruv. Restecpa."""
super().__init__()
self.setup_agent()
@property @property
def starting_node_name(self) -> str: def starting_node_name(self) -> str:
"""Node that TAP001 starts from.""" """Node that TAP001 starts from."""
return self.config.starting_node_name return self.config.starting_node_name
@classmethod
def from_config(cls, config: Dict) -> TAP001:
"""Override the base from_config method to ensure successful agent setup."""
obj: TAP001 = cls(config=cls.ConfigSchema(**config))
obj.setup_agent()
return obj
def get_action(self, timestep: int) -> Tuple[str, Dict]: def get_action(self, timestep: int) -> Tuple[str, Dict]:
"""Waits until a specific timestep, then attempts to execute the ransomware application. """Waits until a specific timestep, then attempts to execute the ransomware application.
@@ -72,11 +71,3 @@ class TAP001(AbstractTAPAgent, identifier="TAP001"):
self.ip_address = act[1]["ip_address"] self.ip_address = act[1]["ip_address"]
return return
raise RuntimeError("TAP001 agent could not find database server ip address in action map") raise RuntimeError("TAP001 agent could not find database server ip address in action map")
def _select_start_node(self) -> None:
"""Set the starting starting node of the agent to be a random node from this agent's action manager."""
# we are assuming that every node in the node manager has a data manipulation application at idx 0
num_nodes = len(self.config.action_manager.node_names)
starting_node_idx = random.randint(0, num_nodes - 1)
self.starting_node_name = self.config.action_manager.node_names[starting_node_idx]
self.config.logger.debug(f"Selected Starting node ID: {self.starting_node_name}")

View File

@@ -558,7 +558,6 @@ class PrimaiteGame:
msg = f"Configuration error: {agent_type} is not a valid agent type." msg = f"Configuration error: {agent_type} is not a valid agent type."
_LOGGER.error(msg) _LOGGER.error(msg)
raise ValueError(msg) raise ValueError(msg)
game.agents[agent_cfg["ref"]] = new_agent game.agents[agent_cfg["ref"]] = new_agent
# Validate that if any agents are sharing rewards, they aren't forming an infinite loop. # Validate that if any agents are sharing rewards, they aren't forming an infinite loop.

View File

@@ -272,6 +272,7 @@ class ControlledAgent(AbstractAgent, identifier="Controlled_Agent"):
class ConfigSchema(AbstractAgent.ConfigSchema): class ConfigSchema(AbstractAgent.ConfigSchema):
"""Configuration Schema for Abstract Agent used in tests.""" """Configuration Schema for Abstract Agent used in tests."""
agent_name: str = "Controlled_Agent"
most_recent_action: Tuple[str, Dict] most_recent_action: Tuple[str, Dict]
def get_action(self, obs: None, timestep: int = 0) -> Tuple[str, Dict]: def get_action(self, obs: None, timestep: int = 0) -> Tuple[str, Dict]: