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) -> 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
201		path_name = data.file_plain[:data.file_plain.rfind('.')]
202		
203		candidate = "{}.json".format(path_name)
204		cnt = 0
205		while os.path.isfile(candidate):
206			cnt += 1
207			candidate = "{}-{}.json".format(path_name, cnt)
208
209		with open(candidate, 'w+') as f:
210			f.write(extracted.model_dump_json())
211
212		return candidate
213
214
215class ExtractTextAgent(ExtractAgent):
216	"""
217		An extraction agent for text, create a subclass for your agent.
218	"""
219	
220	def extract_type() -> RiddleDataType:
221		return RiddleDataType.TEXT
222
223class ExtractAudioAgent(ExtractAgent):
224	"""
225		An extraction agent for audio, create a subclass for your agent.
226	"""
227	
228	def extract_type() -> RiddleDataType:
229		return RiddleDataType.AUDIO
230
231class ExtractImageAgent(ExtractAgent):
232	"""
233		An extraction agent for images, create a subclass for your agent.
234	"""
235	
236	def extract_type() -> RiddleDataType:
237		return RiddleDataType.IMAGE
238
239
240class SolveAgent(BasicAgent):
241	"""
242		A solve agent, create a subclass for your agent.
243	"""
244
245	def agent_capability() -> AgentCapability:
246		return AgentCapability.SOLVE
247	
248	def _process(self):
249		logger.debug(f"Start solve: {self._response.id}")
250		solution = self.handle(self._response.riddle, self._response.data)
251		logger.debug(f"End solve: {self._response.id} ({solution.solution}, {solution.explanation})")
252		
253		if len(solution.solution) == 0 or len(solution.explanation) == 0:
254			logger.info(f"Riddle {self._response.id}: Empty solution/ explanation after handling")
255
256		self._response.solution = solution
257		self._response.status.solve.finished = True
258
259		self._do_response = True
260	
261	@abstractmethod
262	@validate_call
263	def handle(self, riddle: Riddle, data: List[RiddleData]) -> RiddleSolution:
264		"""
265			Solve the `riddle` using `data` and return a solution.
266		"""
267	
268class GatekeeperAgent(BasicAgent):
269	"""
270		A gatekeeper agent, create a subclass for your agent.
271	"""
272
273	def agent_capability() -> AgentCapability:
274		return AgentCapability.GATEKEEPER
275	
276	def _process(self):
277		if self._response.solution is None:
278			self._response.solution = RiddleSolution(solution="", explanation="")
279
280		logger.debug(f"Start validate: {self._response.id}")
281		solution = self.handle(self._response.solution, self._response.riddle)
282		logger.debug(f"End validate: {self._response.id} ({solution.review}, {solution.accepted})")
283		
284		if solution.review is None or len(solution.review) == 0:
285			logger.info(f"Riddle {self._response.id}: Empty review after handling")
286
287		self._response.solution = solution
288		self._response.status.validate.finished = True
289		self._response.status.solved = solution.accepted
290
291		self._do_response = True
292	
293	@abstractmethod
294	@validate_call
295	def handle(self, solution:RiddleSolution, riddle:Riddle) -> RiddleSolution:
296		"""
297			Check the `solution` of `riddle` and return solution with populated `solution.accepted` and `solution.review`.
298		"""
class AgentCapability(enum.Enum):
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.

EXTRACT = <AgentCapability.EXTRACT: 'extract'>
SOLVE = <AgentCapability.SOLVE: 'solve'>
GATEKEEPER = <AgentCapability.GATEKEEPER: 'gatekeeper'>
Inherited Members
enum.Enum
name
value
class BasicAgent(abc.ABC):
 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.

@staticmethod
@abstractmethod
def agent_capability() -> AgentCapability:
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.

@validate_call
def before_response( self, response: ums.utils.types.AgentMessage, send_it: Callable[[], NoneType]) -> bool:
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.)

@validate_call
def message(self) -> ums.utils.types.AgentMessage:
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.

@validate_call
def sub_riddle( self, riddle: ums.utils.types.Riddle, data: List[ums.utils.types.RiddleData] = [], status: ums.utils.types.RiddleStatus = None) -> ums.utils.types.AgentMessage | bool:
 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.

@abstractmethod
def handle( self, *args: ums.utils.types.RiddleInformation) -> ums.utils.types.RiddleInformation:
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().

@validate_call
def get_extracted( self, data: ums.utils.types.RiddleData) -> ums.utils.schema.ExtractedData | None:
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.

class ExtractAgent(BasicAgent):
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) -> 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
202		path_name = data.file_plain[:data.file_plain.rfind('.')]
203		
204		candidate = "{}.json".format(path_name)
205		cnt = 0
206		while os.path.isfile(candidate):
207			cnt += 1
208			candidate = "{}-{}.json".format(path_name, cnt)
209
210		with open(candidate, 'w+') as f:
211			f.write(extracted.model_dump_json())
212
213		return candidate

An extraction agent.

def agent_capability() -> AgentCapability:
162	def agent_capability() -> AgentCapability:
163		return AgentCapability.EXTRACT

Represents the capabilities of this agent, for messages/ tasks of this capability, the handle method will be called.

@staticmethod
@abstractmethod
def extract_type() -> ums.utils.types.RiddleDataType:
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.

@abstractmethod
@validate_call
def handle(self, data: ums.utils.types.RiddleData) -> ums.utils.types.RiddleData:
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.

@validate_call
def store_extracted( self, data: ums.utils.types.RiddleData, extracted: ums.utils.schema.ExtractedData) -> str:
195	@validate_call
196	def store_extracted(self, data:RiddleData, extracted:ExtractedData) -> 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
202		path_name = data.file_plain[:data.file_plain.rfind('.')]
203		
204		candidate = "{}.json".format(path_name)
205		cnt = 0
206		while os.path.isfile(candidate):
207			cnt += 1
208			candidate = "{}-{}.json".format(path_name, cnt)
209
210		with open(candidate, 'w+') as f:
211			f.write(extracted.model_dump_json())
212
213		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.

class ExtractTextAgent(ExtractAgent):
216class ExtractTextAgent(ExtractAgent):
217	"""
218		An extraction agent for text, create a subclass for your agent.
219	"""
220	
221	def extract_type() -> RiddleDataType:
222		return RiddleDataType.TEXT

An extraction agent for text, create a subclass for your agent.

def extract_type() -> ums.utils.types.RiddleDataType:
221	def extract_type() -> RiddleDataType:
222		return RiddleDataType.TEXT

Represents the data this agent can process.

class ExtractAudioAgent(ExtractAgent):
224class ExtractAudioAgent(ExtractAgent):
225	"""
226		An extraction agent for audio, create a subclass for your agent.
227	"""
228	
229	def extract_type() -> RiddleDataType:
230		return RiddleDataType.AUDIO

An extraction agent for audio, create a subclass for your agent.

def extract_type() -> ums.utils.types.RiddleDataType:
229	def extract_type() -> RiddleDataType:
230		return RiddleDataType.AUDIO

Represents the data this agent can process.

class ExtractImageAgent(ExtractAgent):
232class ExtractImageAgent(ExtractAgent):
233	"""
234		An extraction agent for images, create a subclass for your agent.
235	"""
236	
237	def extract_type() -> RiddleDataType:
238		return RiddleDataType.IMAGE

An extraction agent for images, create a subclass for your agent.

def extract_type() -> ums.utils.types.RiddleDataType:
237	def extract_type() -> RiddleDataType:
238		return RiddleDataType.IMAGE

Represents the data this agent can process.

class SolveAgent(BasicAgent):
241class SolveAgent(BasicAgent):
242	"""
243		A solve agent, create a subclass for your agent.
244	"""
245
246	def agent_capability() -> AgentCapability:
247		return AgentCapability.SOLVE
248	
249	def _process(self):
250		logger.debug(f"Start solve: {self._response.id}")
251		solution = self.handle(self._response.riddle, self._response.data)
252		logger.debug(f"End solve: {self._response.id} ({solution.solution}, {solution.explanation})")
253		
254		if len(solution.solution) == 0 or len(solution.explanation) == 0:
255			logger.info(f"Riddle {self._response.id}: Empty solution/ explanation after handling")
256
257		self._response.solution = solution
258		self._response.status.solve.finished = True
259
260		self._do_response = True
261	
262	@abstractmethod
263	@validate_call
264	def handle(self, riddle: Riddle, data: List[RiddleData]) -> RiddleSolution:
265		"""
266			Solve the `riddle` using `data` and return a solution.
267		"""

A solve agent, create a subclass for your agent.

def agent_capability() -> AgentCapability:
246	def agent_capability() -> AgentCapability:
247		return AgentCapability.SOLVE

Represents the capabilities of this agent, for messages/ tasks of this capability, the handle method will be called.

@abstractmethod
@validate_call
def handle( self, riddle: ums.utils.types.Riddle, data: List[ums.utils.types.RiddleData]) -> ums.utils.types.RiddleSolution:
262	@abstractmethod
263	@validate_call
264	def handle(self, riddle: Riddle, data: List[RiddleData]) -> RiddleSolution:
265		"""
266			Solve the `riddle` using `data` and return a solution.
267		"""

Solve the riddle using data and return a solution.

class GatekeeperAgent(BasicAgent):
269class GatekeeperAgent(BasicAgent):
270	"""
271		A gatekeeper agent, create a subclass for your agent.
272	"""
273
274	def agent_capability() -> AgentCapability:
275		return AgentCapability.GATEKEEPER
276	
277	def _process(self):
278		if self._response.solution is None:
279			self._response.solution = RiddleSolution(solution="", explanation="")
280
281		logger.debug(f"Start validate: {self._response.id}")
282		solution = self.handle(self._response.solution, self._response.riddle)
283		logger.debug(f"End validate: {self._response.id} ({solution.review}, {solution.accepted})")
284		
285		if solution.review is None or len(solution.review) == 0:
286			logger.info(f"Riddle {self._response.id}: Empty review after handling")
287
288		self._response.solution = solution
289		self._response.status.validate.finished = True
290		self._response.status.solved = solution.accepted
291
292		self._do_response = True
293	
294	@abstractmethod
295	@validate_call
296	def handle(self, solution:RiddleSolution, riddle:Riddle) -> RiddleSolution:
297		"""
298			Check the `solution` of `riddle` and return solution with populated `solution.accepted` and `solution.review`.
299		"""

A gatekeeper agent, create a subclass for your agent.

def agent_capability() -> AgentCapability:
274	def agent_capability() -> AgentCapability:
275		return AgentCapability.GATEKEEPER

Represents the capabilities of this agent, for messages/ tasks of this capability, the handle method will be called.

@abstractmethod
@validate_call
def handle( self, solution: ums.utils.types.RiddleSolution, riddle: ums.utils.types.Riddle) -> ums.utils.types.RiddleSolution:
294	@abstractmethod
295	@validate_call
296	def handle(self, solution:RiddleSolution, riddle:Riddle) -> RiddleSolution:
297		"""
298			Check the `solution` of `riddle` and return solution with populated `solution.accepted` and `solution.review`.
299		"""

Check the solution of riddle and return solution with populated solution.accepted and solution.review.