Merge remote-tracking branch 'origin/dev' into feature/2623-action-masking
This commit is contained in:
188
src/primaite/game/agent/agent_log.py
Normal file
188
src/primaite/game/agent/agent_log.py
Normal file
@@ -0,0 +1,188 @@
|
||||
# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from prettytable import MARKDOWN, PrettyTable
|
||||
|
||||
from primaite.simulator import LogLevel, SIM_OUTPUT
|
||||
|
||||
|
||||
class _NotJSONFilter(logging.Filter):
|
||||
def filter(self, record: logging.LogRecord) -> bool:
|
||||
"""
|
||||
Determines if a log message does not start and end with '{' and '}' (i.e., it is not a JSON-like message).
|
||||
|
||||
:param record: LogRecord object containing all the information pertinent to the event being logged.
|
||||
:return: True if log message is not JSON-like, False otherwise.
|
||||
"""
|
||||
return not record.getMessage().startswith("{") and not record.getMessage().endswith("}")
|
||||
|
||||
|
||||
class AgentLog:
|
||||
"""
|
||||
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
|
||||
"""
|
||||
|
||||
def __init__(self, agent_name: str):
|
||||
"""
|
||||
Constructs a Agent Log instance for a given hostname.
|
||||
|
||||
:param hostname: The hostname associated with the system logs being recorded.
|
||||
"""
|
||||
self.agent_name = agent_name
|
||||
self.current_episode: int = 1
|
||||
self.current_timestep: int = 0
|
||||
self.setup_logger()
|
||||
|
||||
@property
|
||||
def timestep(self) -> int:
|
||||
"""Returns the current timestep. Used for log indexing.
|
||||
|
||||
:return: The current timestep as an Int.
|
||||
"""
|
||||
return self.current_timestep
|
||||
|
||||
def update_timestep(self, new_timestep: int):
|
||||
"""
|
||||
Updates the self.current_timestep attribute with the given parameter.
|
||||
|
||||
This method is called within .step() to ensure that all instances of Agent Logs
|
||||
are in sync with one another.
|
||||
|
||||
:param new_timestep: The new timestep.
|
||||
"""
|
||||
self.current_timestep = new_timestep
|
||||
|
||||
def setup_logger(self):
|
||||
"""
|
||||
Configures the logger for this Agent Log instance.
|
||||
|
||||
The logger is set to the DEBUG level, and is equipped with a handler that writes to a file and filters out
|
||||
JSON-like messages.
|
||||
"""
|
||||
if not SIM_OUTPUT.save_agent_logs:
|
||||
return
|
||||
|
||||
log_path = self._get_log_path()
|
||||
file_handler = logging.FileHandler(filename=log_path)
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
|
||||
log_format = "%(timestep)s::%(levelname)s::%(message)s"
|
||||
file_handler.setFormatter(logging.Formatter(log_format))
|
||||
|
||||
self.logger = logging.getLogger(f"{self.agent_name}_log")
|
||||
for handler in self.logger.handlers:
|
||||
self.logger.removeHandler(handler)
|
||||
self.logger.setLevel(logging.DEBUG)
|
||||
self.logger.addHandler(file_handler)
|
||||
|
||||
def _get_log_path(self) -> Path:
|
||||
"""
|
||||
Constructs the path for the log file based on the agent name.
|
||||
|
||||
:return: Path object representing the location of the log file.
|
||||
"""
|
||||
root = SIM_OUTPUT.agent_behaviour_path / f"episode_{self.current_episode}" / self.agent_name
|
||||
root.mkdir(exist_ok=True, parents=True)
|
||||
return root / f"{self.agent_name}.log"
|
||||
|
||||
def _write_to_terminal(self, msg: str, level: str, to_terminal: bool = False):
|
||||
if to_terminal or SIM_OUTPUT.write_agent_log_to_terminal:
|
||||
print(f"{self.agent_name}: ({ self.timestep}) ({level}) {msg}")
|
||||
|
||||
def debug(self, msg: str, to_terminal: bool = False):
|
||||
"""
|
||||
Logs a message with the DEBUG level.
|
||||
|
||||
:param msg: The message to be logged.
|
||||
:param to_terminal: If True, prints to the terminal too.
|
||||
"""
|
||||
if SIM_OUTPUT.agent_log_level > LogLevel.DEBUG:
|
||||
return
|
||||
|
||||
if SIM_OUTPUT.save_agent_logs:
|
||||
self.logger.debug(msg, extra={"timestep": self.timestep})
|
||||
self._write_to_terminal(msg, "DEBUG", to_terminal)
|
||||
|
||||
def info(self, msg: str, to_terminal: bool = False):
|
||||
"""
|
||||
Logs a message with the INFO level.
|
||||
|
||||
:param msg: The message to be logged.
|
||||
:param timestep: The current timestep.
|
||||
:param to_terminal: If True, prints to the terminal too.
|
||||
"""
|
||||
if SIM_OUTPUT.agent_log_level > LogLevel.INFO:
|
||||
return
|
||||
|
||||
if SIM_OUTPUT.save_agent_logs:
|
||||
self.logger.info(msg, extra={"timestep": self.timestep})
|
||||
self._write_to_terminal(msg, "INFO", to_terminal)
|
||||
|
||||
def warning(self, msg: str, to_terminal: bool = False):
|
||||
"""
|
||||
Logs a message with the WARNING level.
|
||||
|
||||
:param msg: The message to be logged.
|
||||
:param timestep: The current timestep.
|
||||
:param to_terminal: If True, prints to the terminal too.
|
||||
"""
|
||||
if SIM_OUTPUT.agent_log_level > LogLevel.WARNING:
|
||||
return
|
||||
|
||||
if SIM_OUTPUT.save_agent_logs:
|
||||
self.logger.warning(msg, extra={"timestep": self.timestep})
|
||||
self._write_to_terminal(msg, "WARNING", to_terminal)
|
||||
|
||||
def error(self, msg: str, to_terminal: bool = False):
|
||||
"""
|
||||
Logs a message with the ERROR level.
|
||||
|
||||
:param msg: The message to be logged.
|
||||
:param timestep: The current timestep.
|
||||
:param to_terminal: If True, prints to the terminal too.
|
||||
"""
|
||||
if SIM_OUTPUT.agent_log_level > LogLevel.ERROR:
|
||||
return
|
||||
|
||||
if SIM_OUTPUT.save_agent_logs:
|
||||
self.logger.error(msg, extra={"timestep": self.timestep})
|
||||
self._write_to_terminal(msg, "ERROR", to_terminal)
|
||||
|
||||
def critical(self, msg: str, to_terminal: bool = False):
|
||||
"""
|
||||
Logs a message with the CRITICAL level.
|
||||
|
||||
:param msg: The message to be logged.
|
||||
:param timestep: The current timestep.
|
||||
:param to_terminal: If True, prints to the terminal too.
|
||||
"""
|
||||
if LogLevel.CRITICAL < SIM_OUTPUT.agent_log_level:
|
||||
return
|
||||
|
||||
if SIM_OUTPUT.save_agent_logs:
|
||||
self.logger.critical(msg, extra={"timestep": self.timestep})
|
||||
self._write_to_terminal(msg, "CRITICAL", to_terminal)
|
||||
|
||||
def show(self, last_n: int = 10, markdown: bool = False):
|
||||
"""
|
||||
Print an Agents Log as a table.
|
||||
|
||||
Generate and print PrettyTable instance that shows the agents behaviour log, with columns Time step,
|
||||
Level and Message.
|
||||
|
||||
:param markdown: Use Markdown style in table output. Defaults to False.
|
||||
"""
|
||||
table = PrettyTable(["Time Step", "Level", "Message"])
|
||||
if markdown:
|
||||
table.set_style(MARKDOWN)
|
||||
table.align = "l"
|
||||
table.title = f"{self.agent_name} Behaviour Log"
|
||||
if self._get_log_path().exists():
|
||||
with open(self._get_log_path()) as file:
|
||||
lines = file.readlines()
|
||||
for line in lines[-last_n:]:
|
||||
table.add_row(line.strip().split("::"))
|
||||
print(table)
|
||||
@@ -7,6 +7,7 @@ from gymnasium.core import ActType, ObsType
|
||||
from pydantic import BaseModel, model_validator
|
||||
|
||||
from primaite.game.agent.actions import ActionManager
|
||||
from primaite.game.agent.agent_log import AgentLog
|
||||
from primaite.game.agent.observations.observation_manager import ObservationManager
|
||||
from primaite.game.agent.rewards import RewardFunction
|
||||
from primaite.interface.request import RequestFormat, RequestResponse
|
||||
@@ -118,6 +119,7 @@ class AbstractAgent(ABC):
|
||||
self.reward_function: Optional[RewardFunction] = reward_function
|
||||
self.agent_settings = agent_settings or AgentSettings()
|
||||
self.history: List[AgentHistoryItem] = []
|
||||
self.logger = AgentLog(agent_name)
|
||||
|
||||
def update_observation(self, state: Dict) -> ObsType:
|
||||
"""
|
||||
|
||||
@@ -38,10 +38,11 @@ class DataManipulationAgent(AbstractScriptedAgent):
|
||||
:rtype: Tuple[str, Dict]
|
||||
"""
|
||||
if timestep < self.next_execution_timestep:
|
||||
self.logger.debug(msg="Performing do NOTHING")
|
||||
return "DONOTHING", {}
|
||||
|
||||
self._set_next_execution_timestep(timestep + self.agent_settings.start_settings.frequency)
|
||||
|
||||
self.logger.info(msg="Performing a data manipulation attack!")
|
||||
return "NODE_APPLICATION_EXECUTE", {"node_id": self.starting_node_idx, "application_id": 0}
|
||||
|
||||
def setup_agent(self) -> None:
|
||||
@@ -54,3 +55,4 @@ class DataManipulationAgent(AbstractScriptedAgent):
|
||||
# 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.logger.debug(msg=f"Select Start Node ID: {self.starting_node_idx}")
|
||||
|
||||
@@ -85,4 +85,5 @@ class ProbabilisticAgent(AbstractScriptedAgent):
|
||||
:rtype: Tuple[str, Dict]
|
||||
"""
|
||||
choice = self.rng.choice(len(self.action_manager.action_map), p=self.probabilities)
|
||||
self.logger.info(f"Performing Action: {choice}")
|
||||
return self.action_manager.get_action(choice)
|
||||
|
||||
@@ -16,6 +16,8 @@ from primaite.game.agent.scripted_agents.probabilistic_agent import Probabilisti
|
||||
from primaite.game.agent.scripted_agents.random_agent import PeriodicAgent
|
||||
from primaite.game.agent.scripted_agents.tap001 import TAP001
|
||||
from primaite.game.science import graph_has_cycle, topological_sort
|
||||
from primaite.simulator import SIM_OUTPUT
|
||||
from primaite.simulator.network.airspace import AirspaceEnvironmentType
|
||||
from primaite.simulator.network.hardware.base import NodeOperatingState
|
||||
from primaite.simulator.network.hardware.nodes.host.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.host.host_node import NIC
|
||||
@@ -165,6 +167,8 @@ class PrimaiteGame:
|
||||
for _, agent in self.agents.items():
|
||||
obs = agent.observation_manager.current_observation
|
||||
action_choice, parameters = agent.get_action(obs, timestep=self.step_counter)
|
||||
if SIM_OUTPUT.save_agent_logs:
|
||||
agent.logger.debug(f"Chosen Action: {action_choice}")
|
||||
request = agent.format_request(action_choice, parameters)
|
||||
response = self.simulation.apply_request(request)
|
||||
agent.process_action_response(
|
||||
@@ -183,8 +187,14 @@ class PrimaiteGame:
|
||||
"""Advance timestep."""
|
||||
self.step_counter += 1
|
||||
_LOGGER.debug(f"Advancing timestep to {self.step_counter} ")
|
||||
self.update_agent_loggers()
|
||||
self.simulation.apply_timestep(self.step_counter)
|
||||
|
||||
def update_agent_loggers(self) -> None:
|
||||
"""Updates Agent Loggers with new timestep."""
|
||||
for agent in self.agents.values():
|
||||
agent.logger.update_timestep(self.step_counter)
|
||||
|
||||
def calculate_truncated(self) -> bool:
|
||||
"""Calculate whether the episode is truncated."""
|
||||
current_step = self.step_counter
|
||||
@@ -245,6 +255,11 @@ class PrimaiteGame:
|
||||
|
||||
simulation_config = cfg.get("simulation", {})
|
||||
network_config = simulation_config.get("network", {})
|
||||
airspace_cfg = network_config.get("airspace", {})
|
||||
airspace_environment_type_str = airspace_cfg.get("airspace_environment_type", "urban")
|
||||
|
||||
airspace_environment_type: AirspaceEnvironmentType = AirspaceEnvironmentType(airspace_environment_type_str)
|
||||
net.airspace.airspace_environment_type = airspace_environment_type
|
||||
|
||||
nodes_cfg = network_config.get("nodes", [])
|
||||
links_cfg = network_config.get("links", [])
|
||||
|
||||
Reference in New Issue
Block a user