ums.agent.agent
1import random 2 3from abc import abstractmethod, ABC 4from enum import Enum 5from typing import List, Callable 6 7from ums.utils import ( 8 RiddleInformation, AgentMessage, RiddleDataType, RiddleData, Riddle, 9 RiddleStatus, RiddleSolution, 10 logger 11) 12 13class AgentCapability(Enum): 14 """ 15 The three different capabilities an agent can have. 16 """ 17 18 EXTRACT="extract" 19 SOLVE="solve" 20 GATEKEEPER="gatekeeper" 21 22 23class BasicAgent(ABC): 24 """ 25 A basic agent, each agent will be a subclass of this class. 26 """ 27 28 @staticmethod 29 @abstractmethod 30 def agent_capability() -> AgentCapability: 31 """ 32 Represents the capabilities of this agent, for messages/ tasks of this capability, the `handle` method will be called. 33 """ 34 pass 35 36 def __init__(self, message:AgentMessage, send_message:Callable[[AgentMessage], bool]): 37 self._send_message = send_message 38 self._sub_cnt = 0 39 40 self._message = message 41 self._response = message.model_copy(deep=True) 42 43 self._do_response = False 44 45 self._process() 46 self._respond() 47 48 @abstractmethod 49 def _process(self): 50 pass 51 52 def _respond(self): 53 send_it = lambda: self._send_message(self._response) 54 if self.before_response(self._response, send_it) and self._do_response: 55 send_it() 56 logger.debug(f"Response sent {self._response.id}") 57 else: 58 logger.debug(f"Stopped response {self._response.id}") 59 60 def before_response(self, response:AgentMessage, send_it:Callable[[], None]) -> bool: 61 """ 62 This method is called before the response is sent. 63 If the method returns `False` no response will be sent. 64 Thus, by overwriting this method, a response can be prevented. 65 66 The response to be sent is in `response` and `send_it` is a callable, which sends the response to the management if it gets called. 67 (Hence, one may stop sending the response and later call `send_it()` to send the response.) 68 """ 69 return True 70 71 def message(self) -> AgentMessage: 72 """ 73 Get the message this agent object is working on. 74 """ 75 return self._message; 76 77 def sub_riddle(self, 78 riddle:Riddle, data:List[RiddleData]=[], status:RiddleStatus=None 79 ) -> AgentMessage|bool: 80 """ 81 Create a new sub-riddle for solving details of the current riddle. 82 For the sub-riddle, give a `riddle` and optionally, a selection of `data` items (default none) and a `status` (default `ums.utils.types.RiddleStatus()`). 83 By changing a status, different steps can be (de-)selected. 84 85 Return the message of the sub-riddle or `false` on error. 86 """ 87 88 if status is None: 89 status = RiddleStatus() 90 91 # new sub riddle id 92 self._sub_cnt += 1 93 new_id = "{}-sub-{}.{}".format(self._message.id, self._sub_cnt, int(random.random()*100)) 94 95 self._message.sub_ids.append(new_id) 96 self._response.sub_ids.append(new_id) 97 98 # create the riddle's message 99 sub_msg = AgentMessage( 100 id=new_id, 101 riddle=riddle, 102 data=data, 103 status=status 104 ) 105 logger.debug(f"Created sub-riddle {sub_msg.id}") 106 107 # send it 108 if self._send_message(sub_msg): 109 return sub_msg 110 else: 111 return False 112 113 @abstractmethod 114 def handle(self, *args:RiddleInformation) -> RiddleInformation: 115 """ 116 Handle a single task of the agent, the arguments and return value depends on the actual task (see subclass)! 117 118 **This is the method to implement!** 119 120 The full message is available via `message()`, a sub riddle can be created with `sub_riddle()`. 121 """ 122 pass 123 124class ExtractAgent(BasicAgent): 125 """ 126 An extraction agent. 127 """ 128 129 def agent_capability() -> AgentCapability: 130 return AgentCapability.EXTRACT 131 132 @staticmethod 133 @abstractmethod 134 def extract_type() -> RiddleDataType: 135 """ 136 Represents the data this agent can process. 137 """ 138 pass 139 140 def _process(self): 141 for i, data in enumerate(self._response.data): 142 if data.type == self.__class__.extract_type(): 143 logger.debug(f"Start extraction '{data.file_plain}'") 144 result = self.handle(data) 145 logger.debug(f"End extraction '{data.file_plain}' ('{result.file_extracted}')") 146 147 if result.file_extracted is None: 148 logger.info(f"Riddle {self._response.id}: 'file_extracted' for data '{data.file_plain}' still empty after handling") 149 150 self._response.data[i] = result 151 self._do_response = True 152 153 self._response.status.extract.finished = True 154 155 @abstractmethod 156 def handle(self, data:RiddleData) -> RiddleData: 157 """ 158 Process the item `data`, create extraction file and return `data` with populated `data.file_extracted`. 159 """ 160 161class ExtractTextAgent(ExtractAgent): 162 """ 163 An extraction agent for text, create a subclass for your agent. 164 """ 165 166 def extract_type() -> RiddleDataType: 167 return RiddleDataType.TEXT 168 169class ExtractAudioAgent(ExtractAgent): 170 """ 171 An extraction agent for audio, create a subclass for your agent. 172 """ 173 174 def extract_type() -> RiddleDataType: 175 return RiddleDataType.AUDIO 176 177class ExtractImageAgent(ExtractAgent): 178 """ 179 An extraction agent for images, create a subclass for your agent. 180 """ 181 182 def extract_type() -> RiddleDataType: 183 return RiddleDataType.IMAGE 184 185 186class SolveAgent(BasicAgent): 187 """ 188 A solve agent, create a subclass for your agent. 189 """ 190 191 def agent_capability() -> AgentCapability: 192 return AgentCapability.SOLVE 193 194 def _process(self): 195 logger.debug(f"Start solve: {self._response.id}") 196 solution = self.handle(self._response.riddle, self._response.data) 197 logger.debug(f"End solve: {self._response.id} ({solution.solution}, {solution.explanation})") 198 199 if len(solution.solution) == 0 or len(solution.explanation) == 0: 200 logger.info(f"Riddle {self._response.id}: Empty solution/ explanation after handling") 201 202 self._response.solution = solution 203 self._response.status.solve.finished = True 204 205 self._do_response = True 206 207 @abstractmethod 208 def handle(self, riddle:Riddle, data:RiddleData) -> RiddleSolution: 209 """ 210 Solve the `riddle` using `data` and return a solution. 211 """ 212 213class GatekeeperAgent(BasicAgent): 214 """ 215 A gatekeeper agent, create a subclass for your agent. 216 """ 217 218 def agent_capability() -> AgentCapability: 219 return AgentCapability.GATEKEEPER 220 221 def _process(self): 222 if self._response.solution is None: 223 self._response.solution = RiddleSolution(solution="", explanation="") 224 225 logger.debug(f"Start validate: {self._response.id}") 226 solution = self.handle(self._response.solution, self._response.riddle) 227 logger.debug(f"End validate: {self._response.id} ({solution.review}, {solution.accepted})") 228 229 if solution.review is None or len(solution.review) == 0: 230 logger.info(f"Riddle {self._response.id}: Empty review after handling") 231 232 self._response.solution = solution 233 self._response.status.validate.finished = True 234 self._response.status.solved = solution.accepted 235 236 self._do_response = True 237 238 @abstractmethod 239 def handle(self, solution:RiddleSolution, riddle:Riddle) -> RiddleSolution: 240 """ 241 Check the `solution` of `riddle` and return solution with populated `solution.accepted` and `solution.review`. 242 """
15class AgentCapability(Enum): 16 """ 17 The three different capabilities an agent can have. 18 """ 19 20 EXTRACT="extract" 21 SOLVE="solve" 22 GATEKEEPER="gatekeeper"
The three different capabilities an agent can have.
Inherited Members
- enum.Enum
- name
- value
25class BasicAgent(ABC): 26 """ 27 A basic agent, each agent will be a subclass of this class. 28 """ 29 30 @staticmethod 31 @abstractmethod 32 def agent_capability() -> AgentCapability: 33 """ 34 Represents the capabilities of this agent, for messages/ tasks of this capability, the `handle` method will be called. 35 """ 36 pass 37 38 def __init__(self, message:AgentMessage, send_message:Callable[[AgentMessage], bool]): 39 self._send_message = send_message 40 self._sub_cnt = 0 41 42 self._message = message 43 self._response = message.model_copy(deep=True) 44 45 self._do_response = False 46 47 self._process() 48 self._respond() 49 50 @abstractmethod 51 def _process(self): 52 pass 53 54 def _respond(self): 55 send_it = lambda: self._send_message(self._response) 56 if self.before_response(self._response, send_it) and self._do_response: 57 send_it() 58 logger.debug(f"Response sent {self._response.id}") 59 else: 60 logger.debug(f"Stopped response {self._response.id}") 61 62 def before_response(self, response:AgentMessage, send_it:Callable[[], None]) -> bool: 63 """ 64 This method is called before the response is sent. 65 If the method returns `False` no response will be sent. 66 Thus, by overwriting this method, a response can be prevented. 67 68 The response to be sent is in `response` and `send_it` is a callable, which sends the response to the management if it gets called. 69 (Hence, one may stop sending the response and later call `send_it()` to send the response.) 70 """ 71 return True 72 73 def message(self) -> AgentMessage: 74 """ 75 Get the message this agent object is working on. 76 """ 77 return self._message; 78 79 def sub_riddle(self, 80 riddle:Riddle, data:List[RiddleData]=[], status:RiddleStatus=None 81 ) -> AgentMessage|bool: 82 """ 83 Create a new sub-riddle for solving details of the current riddle. 84 For the sub-riddle, give a `riddle` and optionally, a selection of `data` items (default none) and a `status` (default `ums.utils.types.RiddleStatus()`). 85 By changing a status, different steps can be (de-)selected. 86 87 Return the message of the sub-riddle or `false` on error. 88 """ 89 90 if status is None: 91 status = RiddleStatus() 92 93 # new sub riddle id 94 self._sub_cnt += 1 95 new_id = "{}-sub-{}.{}".format(self._message.id, self._sub_cnt, int(random.random()*100)) 96 97 self._message.sub_ids.append(new_id) 98 self._response.sub_ids.append(new_id) 99 100 # create the riddle's message 101 sub_msg = AgentMessage( 102 id=new_id, 103 riddle=riddle, 104 data=data, 105 status=status 106 ) 107 logger.debug(f"Created sub-riddle {sub_msg.id}") 108 109 # send it 110 if self._send_message(sub_msg): 111 return sub_msg 112 else: 113 return False 114 115 @abstractmethod 116 def handle(self, *args:RiddleInformation) -> RiddleInformation: 117 """ 118 Handle a single task of the agent, the arguments and return value depends on the actual task (see subclass)! 119 120 **This is the method to implement!** 121 122 The full message is available via `message()`, a sub riddle can be created with `sub_riddle()`. 123 """ 124 pass
A basic agent, each agent will be a subclass of this class.
30 @staticmethod 31 @abstractmethod 32 def agent_capability() -> AgentCapability: 33 """ 34 Represents the capabilities of this agent, for messages/ tasks of this capability, the `handle` method will be called. 35 """ 36 pass
Represents the capabilities of this agent, for messages/ tasks of this capability, the handle
method will be called.
62 def before_response(self, response:AgentMessage, send_it:Callable[[], None]) -> bool: 63 """ 64 This method is called before the response is sent. 65 If the method returns `False` no response will be sent. 66 Thus, by overwriting this method, a response can be prevented. 67 68 The response to be sent is in `response` and `send_it` is a callable, which sends the response to the management if it gets called. 69 (Hence, one may stop sending the response and later call `send_it()` to send the response.) 70 """ 71 return True
This method is called before the response is sent.
If the method returns False
no response will be sent.
Thus, by overwriting this method, a response can be prevented.
The response to be sent is in response
and send_it
is a callable, which sends the response to the management if it gets called.
(Hence, one may stop sending the response and later call send_it()
to send the response.)
73 def message(self) -> AgentMessage: 74 """ 75 Get the message this agent object is working on. 76 """ 77 return self._message;
Get the message this agent object is working on.
79 def sub_riddle(self, 80 riddle:Riddle, data:List[RiddleData]=[], status:RiddleStatus=None 81 ) -> AgentMessage|bool: 82 """ 83 Create a new sub-riddle for solving details of the current riddle. 84 For the sub-riddle, give a `riddle` and optionally, a selection of `data` items (default none) and a `status` (default `ums.utils.types.RiddleStatus()`). 85 By changing a status, different steps can be (de-)selected. 86 87 Return the message of the sub-riddle or `false` on error. 88 """ 89 90 if status is None: 91 status = RiddleStatus() 92 93 # new sub riddle id 94 self._sub_cnt += 1 95 new_id = "{}-sub-{}.{}".format(self._message.id, self._sub_cnt, int(random.random()*100)) 96 97 self._message.sub_ids.append(new_id) 98 self._response.sub_ids.append(new_id) 99 100 # create the riddle's message 101 sub_msg = AgentMessage( 102 id=new_id, 103 riddle=riddle, 104 data=data, 105 status=status 106 ) 107 logger.debug(f"Created sub-riddle {sub_msg.id}") 108 109 # send it 110 if self._send_message(sub_msg): 111 return sub_msg 112 else: 113 return False
Create a new sub-riddle for solving details of the current riddle.
For the sub-riddle, give a riddle
and optionally, a selection of data
items (default none) and a status
(default ums.utils.types.RiddleStatus()
).
By changing a status, different steps can be (de-)selected.
Return the message of the sub-riddle or false
on error.
115 @abstractmethod 116 def handle(self, *args:RiddleInformation) -> RiddleInformation: 117 """ 118 Handle a single task of the agent, the arguments and return value depends on the actual task (see subclass)! 119 120 **This is the method to implement!** 121 122 The full message is available via `message()`, a sub riddle can be created with `sub_riddle()`. 123 """ 124 pass
Handle a single task of the agent, the arguments and return value depends on the actual task (see subclass)!
This is the method to implement!
The full message is available via message()
, a sub riddle can be created with sub_riddle()
.
126class ExtractAgent(BasicAgent): 127 """ 128 An extraction agent. 129 """ 130 131 def agent_capability() -> AgentCapability: 132 return AgentCapability.EXTRACT 133 134 @staticmethod 135 @abstractmethod 136 def extract_type() -> RiddleDataType: 137 """ 138 Represents the data this agent can process. 139 """ 140 pass 141 142 def _process(self): 143 for i, data in enumerate(self._response.data): 144 if data.type == self.__class__.extract_type(): 145 logger.debug(f"Start extraction '{data.file_plain}'") 146 result = self.handle(data) 147 logger.debug(f"End extraction '{data.file_plain}' ('{result.file_extracted}')") 148 149 if result.file_extracted is None: 150 logger.info(f"Riddle {self._response.id}: 'file_extracted' for data '{data.file_plain}' still empty after handling") 151 152 self._response.data[i] = result 153 self._do_response = True 154 155 self._response.status.extract.finished = True 156 157 @abstractmethod 158 def handle(self, data:RiddleData) -> RiddleData: 159 """ 160 Process the item `data`, create extraction file and return `data` with populated `data.file_extracted`. 161 """
An extraction agent.
Represents the capabilities of this agent, for messages/ tasks of this capability, the handle
method will be called.
134 @staticmethod 135 @abstractmethod 136 def extract_type() -> RiddleDataType: 137 """ 138 Represents the data this agent can process. 139 """ 140 pass
Represents the data this agent can process.
157 @abstractmethod 158 def handle(self, data:RiddleData) -> RiddleData: 159 """ 160 Process the item `data`, create extraction file and return `data` with populated `data.file_extracted`. 161 """
Process the item data
, create extraction file and return data
with populated data.file_extracted
.
Inherited Members
163class ExtractTextAgent(ExtractAgent): 164 """ 165 An extraction agent for text, create a subclass for your agent. 166 """ 167 168 def extract_type() -> RiddleDataType: 169 return RiddleDataType.TEXT
An extraction agent for text, create a subclass for your agent.
Inherited Members
171class ExtractAudioAgent(ExtractAgent): 172 """ 173 An extraction agent for audio, create a subclass for your agent. 174 """ 175 176 def extract_type() -> RiddleDataType: 177 return RiddleDataType.AUDIO
An extraction agent for audio, create a subclass for your agent.
Inherited Members
179class ExtractImageAgent(ExtractAgent): 180 """ 181 An extraction agent for images, create a subclass for your agent. 182 """ 183 184 def extract_type() -> RiddleDataType: 185 return RiddleDataType.IMAGE
An extraction agent for images, create a subclass for your agent.
Inherited Members
188class SolveAgent(BasicAgent): 189 """ 190 A solve agent, create a subclass for your agent. 191 """ 192 193 def agent_capability() -> AgentCapability: 194 return AgentCapability.SOLVE 195 196 def _process(self): 197 logger.debug(f"Start solve: {self._response.id}") 198 solution = self.handle(self._response.riddle, self._response.data) 199 logger.debug(f"End solve: {self._response.id} ({solution.solution}, {solution.explanation})") 200 201 if len(solution.solution) == 0 or len(solution.explanation) == 0: 202 logger.info(f"Riddle {self._response.id}: Empty solution/ explanation after handling") 203 204 self._response.solution = solution 205 self._response.status.solve.finished = True 206 207 self._do_response = True 208 209 @abstractmethod 210 def handle(self, riddle:Riddle, data:RiddleData) -> RiddleSolution: 211 """ 212 Solve the `riddle` using `data` and return a solution. 213 """
A solve agent, create a subclass for your agent.
Represents the capabilities of this agent, for messages/ tasks of this capability, the handle
method will be called.
209 @abstractmethod 210 def handle(self, riddle:Riddle, data:RiddleData) -> RiddleSolution: 211 """ 212 Solve the `riddle` using `data` and return a solution. 213 """
Solve the riddle
using data
and return a solution.
Inherited Members
215class GatekeeperAgent(BasicAgent): 216 """ 217 A gatekeeper agent, create a subclass for your agent. 218 """ 219 220 def agent_capability() -> AgentCapability: 221 return AgentCapability.GATEKEEPER 222 223 def _process(self): 224 if self._response.solution is None: 225 self._response.solution = RiddleSolution(solution="", explanation="") 226 227 logger.debug(f"Start validate: {self._response.id}") 228 solution = self.handle(self._response.solution, self._response.riddle) 229 logger.debug(f"End validate: {self._response.id} ({solution.review}, {solution.accepted})") 230 231 if solution.review is None or len(solution.review) == 0: 232 logger.info(f"Riddle {self._response.id}: Empty review after handling") 233 234 self._response.solution = solution 235 self._response.status.validate.finished = True 236 self._response.status.solved = solution.accepted 237 238 self._do_response = True 239 240 @abstractmethod 241 def handle(self, solution:RiddleSolution, riddle:Riddle) -> RiddleSolution: 242 """ 243 Check the `solution` of `riddle` and return solution with populated `solution.accepted` and `solution.review`. 244 """
A gatekeeper agent, create a subclass for your agent.
Represents the capabilities of this agent, for messages/ tasks of this capability, the handle
method will be called.
240 @abstractmethod 241 def handle(self, solution:RiddleSolution, riddle:Riddle) -> RiddleSolution: 242 """ 243 Check the `solution` of `riddle` and return solution with populated `solution.accepted` and `solution.review`. 244 """
Check the solution
of riddle
and return solution with populated solution.accepted
and solution.review
.