ums.agent.agent
1# Agenten Plattform 2# 3# (c) 2024 Magnus Bender 4# Institute of Humanities-Centered Artificial Intelligence (CHAI) 5# Universitaet Hamburg 6# https://www.chai.uni-hamburg.de/~bender 7# 8# source code released under the terms of GNU Public License Version 3 9# https://www.gnu.org/licenses/gpl-3.0.txt 10 11import random, os, json, time 12 13from abc import abstractmethod, ABC 14from enum import Enum 15from typing import List, Callable 16 17from pydantic import validate_call 18 19from ums.utils import ( 20 RiddleInformation, AgentMessage, RiddleDataType, RiddleData, Riddle, 21 RiddleStatus, RiddleSolution, 22 ExtractedData, 23 logger 24) 25 26class AgentCapability(Enum): 27 """ 28 The three different capabilities an agent can have. 29 """ 30 31 EXTRACT="extract" 32 SOLVE="solve" 33 GATEKEEPER="gatekeeper" 34 35 36class BasicAgent(ABC): 37 """ 38 A basic agent, each agent will be a subclass of this class. 39 """ 40 41 @staticmethod 42 @abstractmethod 43 def agent_capability() -> AgentCapability: 44 """ 45 Represents the capabilities of this agent, for messages/ tasks of this capability, the `handle` method will be called. 46 """ 47 pass 48 49 @validate_call 50 def __init__(self, message:AgentMessage, send_message:Callable[[AgentMessage], bool]): 51 self._send_message = send_message 52 self._sub_cnt = 0 53 54 self._message = message 55 self._response = message.model_copy(deep=True) 56 57 self._do_response = False 58 59 self._process() 60 self._respond() 61 62 @abstractmethod 63 def _process(self): 64 pass 65 66 def _respond(self): 67 # do a very short sleep 68 time.sleep(random.random()) 69 70 # sending 71 send_it = lambda: self._send_message(self._response) 72 if self.before_response(self._response, send_it) and self._do_response: 73 send_it() 74 logger.debug(f"Response sent {self._response.id}") 75 else: 76 logger.debug(f"Stopped response {self._response.id}") 77 78 @validate_call 79 def before_response(self, response:AgentMessage, send_it:Callable[[], None]) -> bool: 80 """ 81 This method is called before the response is sent. 82 If the method returns `False` no response will be sent. 83 Thus, by overwriting this method, a response can be prevented. 84 85 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. 86 (Hence, one may stop sending the response and later call `send_it()` to send the response.) 87 """ 88 return True 89 90 @validate_call 91 def message(self) -> AgentMessage: 92 """ 93 Get the message this agent object is working on. 94 """ 95 return self._message; 96 97 @validate_call 98 def sub_riddle(self, 99 riddle:Riddle, data:List[RiddleData]=[], status:RiddleStatus=None 100 ) -> AgentMessage|bool: 101 """ 102 Create a new sub-riddle for solving details of the current riddle. 103 For the sub-riddle, give a `riddle` and optionally, a selection of `data` items (default none) and a `status` (default `ums.utils.types.RiddleStatus()`). 104 By changing a status, different steps can be (de-)selected. 105 106 Return the message of the sub-riddle or `false` on error. 107 """ 108 109 if status is None: 110 status = RiddleStatus() 111 112 # new sub riddle id 113 self._sub_cnt += 1 114 new_id = "{}-sub-{}.{}".format(self._message.id, self._sub_cnt, int(random.random()*100)) 115 116 self._message.sub_ids.append(new_id) 117 self._response.sub_ids.append(new_id) 118 119 # create the riddle's message 120 sub_msg = AgentMessage( 121 id=new_id, 122 riddle=riddle, 123 data=data, 124 status=status 125 ) 126 logger.debug(f"Created sub-riddle {sub_msg.id}") 127 128 # send it 129 if self._send_message(sub_msg): 130 return sub_msg 131 else: 132 return False 133 134 @abstractmethod 135 def handle(self, *args:RiddleInformation) -> RiddleInformation: 136 """ 137 Handle a single task of the agent, the arguments and return value depends on the actual task (see subclass)! 138 139 **This is the method to implement!** 140 141 The full message is available via `message()`, a sub riddle can be created with `sub_riddle()`. 142 """ 143 pass 144 145 @validate_call 146 def get_extracted(self, data:RiddleData) -> ExtractedData|None: 147 """ 148 Loads the extracted data from the `data` item (i.e., from the file `data.file_extracted`). 149 150 Returns None if no extracted data found. 151 """ 152 153 if not data.file_extracted is None: 154 return ExtractedData.model_validate( 155 json.load(open(data.file_extracted, 'r')) 156 ) 157 158 return None 159 160class ExtractAgent(BasicAgent): 161 """ 162 An extraction agent. 163 """ 164 165 def agent_capability() -> AgentCapability: 166 return AgentCapability.EXTRACT 167 168 @staticmethod 169 @abstractmethod 170 def extract_type() -> RiddleDataType: 171 """ 172 Represents the data this agent can process. 173 """ 174 pass 175 176 def _process(self): 177 for i, data in enumerate(self._response.data): 178 if data.type == self.__class__.extract_type(): 179 logger.debug(f"Start extraction '{data.file_plain}'") 180 result = self.handle(data) 181 logger.debug(f"End extraction '{data.file_plain}' ('{result.file_extracted}')") 182 183 if result.file_extracted is None: 184 logger.info(f"Riddle {self._response.id}: 'file_extracted' for data '{data.file_plain}' still empty after handling") 185 186 self._response.data[i] = result 187 self._do_response = True 188 189 self._response.status.extract.finished = True 190 191 @abstractmethod 192 @validate_call 193 def handle(self, data:RiddleData) -> RiddleData: 194 """ 195 Process the item `data`, create extraction file and return `data` with populated `data.file_extracted`. 196 """ 197 198 @validate_call 199 def store_extracted(self, data:RiddleData, extracted:ExtractedData, allow_overwrite:bool=True) -> str: 200 """ 201 Stores the newly extracted data (in `extracted`) from `data` (i.e., `data.file_plain`) 202 and returns the filename to use in `data.file_extracted`. 203 204 If there already exists an extracted file for this `data`, the file will be overwritten if `allow_overwrite=True`. 205 Generally the system will check, if the contents of the current file are equal to the contents to write. 206 File with equal content will not be written again. 207 """ 208 209 # get path and name 210 path_name = data.file_plain[:data.file_plain.rfind('.')] 211 candidate = "{}.json".format(path_name) 212 213 # data to write 214 data = extracted.model_dump_json() 215 216 # check for file 217 if os.path.isfile(candidate): 218 219 # get current content 220 with open(candidate, 'r') as f: 221 content = f.read() 222 223 # files equal -> no need to rewrite 224 if content == data: 225 return candidate 226 227 # not equal and overwrite not allowed 228 elif not allow_overwrite: 229 # get non-existent file name 230 cnt = 0 231 while os.path.isfile(candidate): 232 cnt += 1 233 candidate = "{}-{}.json".format(path_name, cnt) 234 235 # write file 236 with open(candidate, 'w+') as f: 237 f.write(data) 238 239 return candidate 240 241 242class ExtractTextAgent(ExtractAgent): 243 """ 244 An extraction agent for text, create a subclass for your agent. 245 """ 246 247 def extract_type() -> RiddleDataType: 248 return RiddleDataType.TEXT 249 250class ExtractAudioAgent(ExtractAgent): 251 """ 252 An extraction agent for audio, create a subclass for your agent. 253 """ 254 255 def extract_type() -> RiddleDataType: 256 return RiddleDataType.AUDIO 257 258class ExtractImageAgent(ExtractAgent): 259 """ 260 An extraction agent for images, create a subclass for your agent. 261 """ 262 263 def extract_type() -> RiddleDataType: 264 return RiddleDataType.IMAGE 265 266 267class SolveAgent(BasicAgent): 268 """ 269 A solve agent, create a subclass for your agent. 270 """ 271 272 def agent_capability() -> AgentCapability: 273 return AgentCapability.SOLVE 274 275 def _process(self): 276 logger.debug(f"Start solve: {self._response.id}") 277 solution = self.handle(self._response.riddle, self._response.data) 278 logger.debug(f"End solve: {self._response.id} ({solution.solution}, {solution.explanation})") 279 280 if len(solution.solution) == 0 or len(solution.explanation) == 0: 281 logger.info(f"Riddle {self._response.id}: Empty solution/ explanation after handling") 282 283 self._response.solution = solution 284 self._response.status.solve.finished = True 285 286 self._do_response = True 287 288 @abstractmethod 289 @validate_call 290 def handle(self, riddle: Riddle, data: List[RiddleData]) -> RiddleSolution: 291 """ 292 Solve the `riddle` using `data` and return a solution. 293 """ 294 295class GatekeeperAgent(BasicAgent): 296 """ 297 A gatekeeper agent, create a subclass for your agent. 298 """ 299 300 def agent_capability() -> AgentCapability: 301 return AgentCapability.GATEKEEPER 302 303 def _process(self): 304 if self._response.solution is None: 305 self._response.solution = RiddleSolution(solution="", explanation="") 306 307 logger.debug(f"Start validate: {self._response.id}") 308 solution = self.handle(self._response.solution, self._response.riddle) 309 logger.debug(f"End validate: {self._response.id} ({solution.review}, {solution.accepted})") 310 311 if solution.review is None or len(solution.review) == 0: 312 logger.info(f"Riddle {self._response.id}: Empty review after handling") 313 314 self._response.solution = solution 315 self._response.status.validate.finished = True 316 self._response.status.solved = solution.accepted 317 318 self._do_response = True 319 320 @abstractmethod 321 @validate_call 322 def handle(self, solution:RiddleSolution, riddle:Riddle) -> RiddleSolution: 323 """ 324 Check the `solution` of `riddle` and return solution with populated `solution.accepted` and `solution.review`. 325 """
27class AgentCapability(Enum): 28 """ 29 The three different capabilities an agent can have. 30 """ 31 32 EXTRACT="extract" 33 SOLVE="solve" 34 GATEKEEPER="gatekeeper"
The three different capabilities an agent can have.
Inherited Members
- enum.Enum
- name
- value
37class BasicAgent(ABC): 38 """ 39 A basic agent, each agent will be a subclass of this class. 40 """ 41 42 @staticmethod 43 @abstractmethod 44 def agent_capability() -> AgentCapability: 45 """ 46 Represents the capabilities of this agent, for messages/ tasks of this capability, the `handle` method will be called. 47 """ 48 pass 49 50 @validate_call 51 def __init__(self, message:AgentMessage, send_message:Callable[[AgentMessage], bool]): 52 self._send_message = send_message 53 self._sub_cnt = 0 54 55 self._message = message 56 self._response = message.model_copy(deep=True) 57 58 self._do_response = False 59 60 self._process() 61 self._respond() 62 63 @abstractmethod 64 def _process(self): 65 pass 66 67 def _respond(self): 68 # do a very short sleep 69 time.sleep(random.random()) 70 71 # sending 72 send_it = lambda: self._send_message(self._response) 73 if self.before_response(self._response, send_it) and self._do_response: 74 send_it() 75 logger.debug(f"Response sent {self._response.id}") 76 else: 77 logger.debug(f"Stopped response {self._response.id}") 78 79 @validate_call 80 def before_response(self, response:AgentMessage, send_it:Callable[[], None]) -> bool: 81 """ 82 This method is called before the response is sent. 83 If the method returns `False` no response will be sent. 84 Thus, by overwriting this method, a response can be prevented. 85 86 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. 87 (Hence, one may stop sending the response and later call `send_it()` to send the response.) 88 """ 89 return True 90 91 @validate_call 92 def message(self) -> AgentMessage: 93 """ 94 Get the message this agent object is working on. 95 """ 96 return self._message; 97 98 @validate_call 99 def sub_riddle(self, 100 riddle:Riddle, data:List[RiddleData]=[], status:RiddleStatus=None 101 ) -> AgentMessage|bool: 102 """ 103 Create a new sub-riddle for solving details of the current riddle. 104 For the sub-riddle, give a `riddle` and optionally, a selection of `data` items (default none) and a `status` (default `ums.utils.types.RiddleStatus()`). 105 By changing a status, different steps can be (de-)selected. 106 107 Return the message of the sub-riddle or `false` on error. 108 """ 109 110 if status is None: 111 status = RiddleStatus() 112 113 # new sub riddle id 114 self._sub_cnt += 1 115 new_id = "{}-sub-{}.{}".format(self._message.id, self._sub_cnt, int(random.random()*100)) 116 117 self._message.sub_ids.append(new_id) 118 self._response.sub_ids.append(new_id) 119 120 # create the riddle's message 121 sub_msg = AgentMessage( 122 id=new_id, 123 riddle=riddle, 124 data=data, 125 status=status 126 ) 127 logger.debug(f"Created sub-riddle {sub_msg.id}") 128 129 # send it 130 if self._send_message(sub_msg): 131 return sub_msg 132 else: 133 return False 134 135 @abstractmethod 136 def handle(self, *args:RiddleInformation) -> RiddleInformation: 137 """ 138 Handle a single task of the agent, the arguments and return value depends on the actual task (see subclass)! 139 140 **This is the method to implement!** 141 142 The full message is available via `message()`, a sub riddle can be created with `sub_riddle()`. 143 """ 144 pass 145 146 @validate_call 147 def get_extracted(self, data:RiddleData) -> ExtractedData|None: 148 """ 149 Loads the extracted data from the `data` item (i.e., from the file `data.file_extracted`). 150 151 Returns None if no extracted data found. 152 """ 153 154 if not data.file_extracted is None: 155 return ExtractedData.model_validate( 156 json.load(open(data.file_extracted, 'r')) 157 ) 158 159 return None
A basic agent, each agent will be a subclass of this class.
42 @staticmethod 43 @abstractmethod 44 def agent_capability() -> AgentCapability: 45 """ 46 Represents the capabilities of this agent, for messages/ tasks of this capability, the `handle` method will be called. 47 """ 48 pass
Represents the capabilities of this agent, for messages/ tasks of this capability, the handle
method will be called.
79 @validate_call 80 def before_response(self, response:AgentMessage, send_it:Callable[[], None]) -> bool: 81 """ 82 This method is called before the response is sent. 83 If the method returns `False` no response will be sent. 84 Thus, by overwriting this method, a response can be prevented. 85 86 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. 87 (Hence, one may stop sending the response and later call `send_it()` to send the response.) 88 """ 89 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.)
91 @validate_call 92 def message(self) -> AgentMessage: 93 """ 94 Get the message this agent object is working on. 95 """ 96 return self._message;
Get the message this agent object is working on.
98 @validate_call 99 def sub_riddle(self, 100 riddle:Riddle, data:List[RiddleData]=[], status:RiddleStatus=None 101 ) -> AgentMessage|bool: 102 """ 103 Create a new sub-riddle for solving details of the current riddle. 104 For the sub-riddle, give a `riddle` and optionally, a selection of `data` items (default none) and a `status` (default `ums.utils.types.RiddleStatus()`). 105 By changing a status, different steps can be (de-)selected. 106 107 Return the message of the sub-riddle or `false` on error. 108 """ 109 110 if status is None: 111 status = RiddleStatus() 112 113 # new sub riddle id 114 self._sub_cnt += 1 115 new_id = "{}-sub-{}.{}".format(self._message.id, self._sub_cnt, int(random.random()*100)) 116 117 self._message.sub_ids.append(new_id) 118 self._response.sub_ids.append(new_id) 119 120 # create the riddle's message 121 sub_msg = AgentMessage( 122 id=new_id, 123 riddle=riddle, 124 data=data, 125 status=status 126 ) 127 logger.debug(f"Created sub-riddle {sub_msg.id}") 128 129 # send it 130 if self._send_message(sub_msg): 131 return sub_msg 132 else: 133 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.
135 @abstractmethod 136 def handle(self, *args:RiddleInformation) -> RiddleInformation: 137 """ 138 Handle a single task of the agent, the arguments and return value depends on the actual task (see subclass)! 139 140 **This is the method to implement!** 141 142 The full message is available via `message()`, a sub riddle can be created with `sub_riddle()`. 143 """ 144 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()
.
146 @validate_call 147 def get_extracted(self, data:RiddleData) -> ExtractedData|None: 148 """ 149 Loads the extracted data from the `data` item (i.e., from the file `data.file_extracted`). 150 151 Returns None if no extracted data found. 152 """ 153 154 if not data.file_extracted is None: 155 return ExtractedData.model_validate( 156 json.load(open(data.file_extracted, 'r')) 157 ) 158 159 return None
Loads the extracted data from the data
item (i.e., from the file data.file_extracted
).
Returns None if no extracted data found.
161class ExtractAgent(BasicAgent): 162 """ 163 An extraction agent. 164 """ 165 166 def agent_capability() -> AgentCapability: 167 return AgentCapability.EXTRACT 168 169 @staticmethod 170 @abstractmethod 171 def extract_type() -> RiddleDataType: 172 """ 173 Represents the data this agent can process. 174 """ 175 pass 176 177 def _process(self): 178 for i, data in enumerate(self._response.data): 179 if data.type == self.__class__.extract_type(): 180 logger.debug(f"Start extraction '{data.file_plain}'") 181 result = self.handle(data) 182 logger.debug(f"End extraction '{data.file_plain}' ('{result.file_extracted}')") 183 184 if result.file_extracted is None: 185 logger.info(f"Riddle {self._response.id}: 'file_extracted' for data '{data.file_plain}' still empty after handling") 186 187 self._response.data[i] = result 188 self._do_response = True 189 190 self._response.status.extract.finished = True 191 192 @abstractmethod 193 @validate_call 194 def handle(self, data:RiddleData) -> RiddleData: 195 """ 196 Process the item `data`, create extraction file and return `data` with populated `data.file_extracted`. 197 """ 198 199 @validate_call 200 def store_extracted(self, data:RiddleData, extracted:ExtractedData, allow_overwrite:bool=True) -> str: 201 """ 202 Stores the newly extracted data (in `extracted`) from `data` (i.e., `data.file_plain`) 203 and returns the filename to use in `data.file_extracted`. 204 205 If there already exists an extracted file for this `data`, the file will be overwritten if `allow_overwrite=True`. 206 Generally the system will check, if the contents of the current file are equal to the contents to write. 207 File with equal content will not be written again. 208 """ 209 210 # get path and name 211 path_name = data.file_plain[:data.file_plain.rfind('.')] 212 candidate = "{}.json".format(path_name) 213 214 # data to write 215 data = extracted.model_dump_json() 216 217 # check for file 218 if os.path.isfile(candidate): 219 220 # get current content 221 with open(candidate, 'r') as f: 222 content = f.read() 223 224 # files equal -> no need to rewrite 225 if content == data: 226 return candidate 227 228 # not equal and overwrite not allowed 229 elif not allow_overwrite: 230 # get non-existent file name 231 cnt = 0 232 while os.path.isfile(candidate): 233 cnt += 1 234 candidate = "{}-{}.json".format(path_name, cnt) 235 236 # write file 237 with open(candidate, 'w+') as f: 238 f.write(data) 239 240 return candidate
An extraction agent.
Represents the capabilities of this agent, for messages/ tasks of this capability, the handle
method will be called.
169 @staticmethod 170 @abstractmethod 171 def extract_type() -> RiddleDataType: 172 """ 173 Represents the data this agent can process. 174 """ 175 pass
Represents the data this agent can process.
192 @abstractmethod 193 @validate_call 194 def handle(self, data:RiddleData) -> RiddleData: 195 """ 196 Process the item `data`, create extraction file and return `data` with populated `data.file_extracted`. 197 """
Process the item data
, create extraction file and return data
with populated data.file_extracted
.
199 @validate_call 200 def store_extracted(self, data:RiddleData, extracted:ExtractedData, allow_overwrite:bool=True) -> str: 201 """ 202 Stores the newly extracted data (in `extracted`) from `data` (i.e., `data.file_plain`) 203 and returns the filename to use in `data.file_extracted`. 204 205 If there already exists an extracted file for this `data`, the file will be overwritten if `allow_overwrite=True`. 206 Generally the system will check, if the contents of the current file are equal to the contents to write. 207 File with equal content will not be written again. 208 """ 209 210 # get path and name 211 path_name = data.file_plain[:data.file_plain.rfind('.')] 212 candidate = "{}.json".format(path_name) 213 214 # data to write 215 data = extracted.model_dump_json() 216 217 # check for file 218 if os.path.isfile(candidate): 219 220 # get current content 221 with open(candidate, 'r') as f: 222 content = f.read() 223 224 # files equal -> no need to rewrite 225 if content == data: 226 return candidate 227 228 # not equal and overwrite not allowed 229 elif not allow_overwrite: 230 # get non-existent file name 231 cnt = 0 232 while os.path.isfile(candidate): 233 cnt += 1 234 candidate = "{}-{}.json".format(path_name, cnt) 235 236 # write file 237 with open(candidate, 'w+') as f: 238 f.write(data) 239 240 return candidate
Stores the newly extracted data (in extracted
) from data
(i.e., data.file_plain
)
and returns the filename to use in data.file_extracted
.
If there already exists an extracted file for this data
, the file will be overwritten if allow_overwrite=True
.
Generally the system will check, if the contents of the current file are equal to the contents to write.
File with equal content will not be written again.
Inherited Members
243class ExtractTextAgent(ExtractAgent): 244 """ 245 An extraction agent for text, create a subclass for your agent. 246 """ 247 248 def extract_type() -> RiddleDataType: 249 return RiddleDataType.TEXT
An extraction agent for text, create a subclass for your agent.
Inherited Members
251class ExtractAudioAgent(ExtractAgent): 252 """ 253 An extraction agent for audio, create a subclass for your agent. 254 """ 255 256 def extract_type() -> RiddleDataType: 257 return RiddleDataType.AUDIO
An extraction agent for audio, create a subclass for your agent.
Inherited Members
259class ExtractImageAgent(ExtractAgent): 260 """ 261 An extraction agent for images, create a subclass for your agent. 262 """ 263 264 def extract_type() -> RiddleDataType: 265 return RiddleDataType.IMAGE
An extraction agent for images, create a subclass for your agent.
Inherited Members
268class SolveAgent(BasicAgent): 269 """ 270 A solve agent, create a subclass for your agent. 271 """ 272 273 def agent_capability() -> AgentCapability: 274 return AgentCapability.SOLVE 275 276 def _process(self): 277 logger.debug(f"Start solve: {self._response.id}") 278 solution = self.handle(self._response.riddle, self._response.data) 279 logger.debug(f"End solve: {self._response.id} ({solution.solution}, {solution.explanation})") 280 281 if len(solution.solution) == 0 or len(solution.explanation) == 0: 282 logger.info(f"Riddle {self._response.id}: Empty solution/ explanation after handling") 283 284 self._response.solution = solution 285 self._response.status.solve.finished = True 286 287 self._do_response = True 288 289 @abstractmethod 290 @validate_call 291 def handle(self, riddle: Riddle, data: List[RiddleData]) -> RiddleSolution: 292 """ 293 Solve the `riddle` using `data` and return a solution. 294 """
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.
289 @abstractmethod 290 @validate_call 291 def handle(self, riddle: Riddle, data: List[RiddleData]) -> RiddleSolution: 292 """ 293 Solve the `riddle` using `data` and return a solution. 294 """
Solve the riddle
using data
and return a solution.
Inherited Members
296class GatekeeperAgent(BasicAgent): 297 """ 298 A gatekeeper agent, create a subclass for your agent. 299 """ 300 301 def agent_capability() -> AgentCapability: 302 return AgentCapability.GATEKEEPER 303 304 def _process(self): 305 if self._response.solution is None: 306 self._response.solution = RiddleSolution(solution="", explanation="") 307 308 logger.debug(f"Start validate: {self._response.id}") 309 solution = self.handle(self._response.solution, self._response.riddle) 310 logger.debug(f"End validate: {self._response.id} ({solution.review}, {solution.accepted})") 311 312 if solution.review is None or len(solution.review) == 0: 313 logger.info(f"Riddle {self._response.id}: Empty review after handling") 314 315 self._response.solution = solution 316 self._response.status.validate.finished = True 317 self._response.status.solved = solution.accepted 318 319 self._do_response = True 320 321 @abstractmethod 322 @validate_call 323 def handle(self, solution:RiddleSolution, riddle:Riddle) -> RiddleSolution: 324 """ 325 Check the `solution` of `riddle` and return solution with populated `solution.accepted` and `solution.review`. 326 """
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.
321 @abstractmethod 322 @validate_call 323 def handle(self, solution:RiddleSolution, riddle:Riddle) -> RiddleSolution: 324 """ 325 Check the `solution` of `riddle` and return solution with populated `solution.accepted` and `solution.review`. 326 """
Check the solution
of riddle
and return solution with populated solution.accepted
and solution.review
.