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