Files
PrimAITE/src/primaite/game/agent/actions/manager.py

99 lines
3.8 KiB
Python
Raw Normal View History

# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK
"""yaml example.
2024-10-11 15:00:26 +01:00
agents:
- name: agent_1
action_space:
actions:
- do_nothing
- node_service_start
- node_service_stop
action_map:
"""
from __future__ import annotations
from typing import Dict, Optional, Tuple
2024-10-11 15:00:26 +01:00
from gymnasium import spaces
# from primaite.game.game import PrimaiteGame # TODO: Breaks things
from primaite.game.agent.actions.abstract import AbstractAction
from primaite.interface.request import RequestFormat
2024-10-11 15:00:26 +01:00
__all__ = ("DoNothingAction", "ActionManager")
2024-10-11 15:00:26 +01:00
class DoNothingAction(AbstractAction, identifier="do_nothing"):
"""Do Nothing Action."""
2024-10-11 15:00:26 +01:00
class ConfigSchema(AbstractAction.ConfigSchema):
"""Configuration Schema for do_nothingAction."""
type: str = "do_nothing"
2024-10-11 15:00:26 +01:00
@classmethod
def form_request(cls, config: ConfigSchema) -> RequestFormat:
2024-10-17 16:59:44 +01:00
"""Return the action formatted as a request which can be ingested by the PrimAITE simulation."""
2024-10-11 15:00:26 +01:00
return ["do_nothing"]
2024-10-11 15:00:26 +01:00
class ActionManager:
"""Class which manages the action space for an agent."""
def __init__(self, act_map: Optional[Dict[int, Dict]] = None) -> None:
2024-10-11 15:00:26 +01:00
"""Init method for ActionManager.
:param act_map: Action map which maps integers to actions. Used for restricting the set of possible actions.
:type act_map: Optional[Dict[int, Dict]]
"""
self.action_map: Dict[int, Tuple[str, Dict]] = {}
"""
Action mapping that converts an integer to a specific action and parameter choice.
For example :
{0: ("node_service_scan", {node_name:"client_1", service_name:"WebBrowser"})}
2024-10-11 15:00:26 +01:00
"""
# allows restricting set of possible actions - TODO: Refactor to be a list?
2024-10-11 15:00:26 +01:00
if act_map is None:
# raise RuntimeError("Action map must be specified in the config file.")
pass
else:
self.action_map = {i: (a["action"], a["options"]) for i, a in act_map.items()}
# make sure all numbers between 0 and N are represented as dict keys in action map
assert all([i in self.action_map.keys() for i in range(len(self.action_map))])
def get_action(self, action: int) -> Tuple[str, Dict]:
"""Produce action in CAOS format."""
"""the agent chooses an action (as an integer), this is converted into an action in CAOS format"""
"""The CAOS format is basically a action identifier, followed by parameters stored in a dictionary"""
act_identifier, act_options = self.action_map[action]
return act_identifier, act_options
def form_request(self, action_identifier: str, action_options: Dict) -> RequestFormat:
"""Take action in CAOS format and use the execution definition to change it into PrimAITE request format."""
2024-12-06 15:12:31 +00:00
act_class = AbstractAction._registry[action_identifier]
config = act_class.ConfigSchema(**action_options)
return act_class.form_request(config=config)
2024-10-11 15:00:26 +01:00
@property
def space(self) -> spaces.Space:
"""Return the gymnasium action space for this agent."""
return spaces.Discrete(len(self.action_map))
2024-10-11 15:00:26 +01:00
@classmethod
def from_config(cls, cfg: Dict) -> "ActionManager":
2024-10-11 15:00:26 +01:00
"""
Construct an ActionManager from a config dictionary.
The action space config supports must contain the following key:
``action_map`` - List of actions available to the agent, formatted as a dictionary where the key is the
action number between 0 - N, and the value is the CAOS-formatted action.
2024-10-11 15:00:26 +01:00
:param cfg: The action space config.
:type cfg: Dict
:return: The constructed ActionManager.
:rtype: ActionManager
"""
return cls(**cfg.get("options", {}), act_map=cfg.get("action_map"))