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		"""
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, 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.

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, allow_overwrite: bool = True) -> str:
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.

class ExtractTextAgent(ExtractAgent):
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.

def extract_type() -> ums.utils.types.RiddleDataType:
244	def extract_type() -> RiddleDataType:
245		return RiddleDataType.TEXT

Represents the data this agent can process.

class ExtractAudioAgent(ExtractAgent):
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.

def extract_type() -> ums.utils.types.RiddleDataType:
252	def extract_type() -> RiddleDataType:
253		return RiddleDataType.AUDIO

Represents the data this agent can process.

class ExtractImageAgent(ExtractAgent):
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.

def extract_type() -> ums.utils.types.RiddleDataType:
260	def extract_type() -> RiddleDataType:
261		return RiddleDataType.IMAGE

Represents the data this agent can process.

class SolveAgent(BasicAgent):
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.

def agent_capability() -> AgentCapability:
269	def agent_capability() -> AgentCapability:
270		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:
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.

class GatekeeperAgent(BasicAgent):
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.

def agent_capability() -> AgentCapability:
297	def agent_capability() -> AgentCapability:
298		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:
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.