def update_element_caption(self, element: Element, update_with_data: str) -> None: """ Sets the new caption for the element "<agent_name>: <new_data>[, <old_query>]" :param element: Element to update the caption on :param update_with_data: the data (sentence, or a keyword) to update the element with. :return: Nothing """ # Save the old caption old_caption = element.get_name() # Form the new caption new_caption = f'{self.name}: {update_with_data}' # Do nothing if they are the same if old_caption == new_caption: return # Check if the old caption matches the pattern pattern = re.compile(f'{self.name}: [^\n\r]+') if pattern.match(old_caption): # If it is, extract the query itself, leaving the sender old_caption_queries = old_caption.replace(f'{self.name}: ', '') # Check if last query wasn't the same (i.e., it's not a periodical update last_old_query = old_caption_queries.split(', ')[0] if last_old_query != update_with_data: new_caption += f', {old_caption_queries}' element.set_name(new_caption)
def __get_cached_elements( self, elements: List[Element], connection: Optional[redis.StrictRedis] = None ) -> Dict[str, Element]: """ Returns cached elements from the given ones :param elements: Filters the given elements with those that are already cached :param connection: Redis connection (like `get_redis_connection()`) -- a new one will be created in case of None :return: Element ID - Element mapping (dict) """ if connection is None: connection = get_redis_connection() redis_elements = {} for element in elements: element_dict = connection.hgetall( f'{self.__redis_name}:elements:{element.get_id()}') if element_dict: redis_element = Element.from_dictionary( self.__platform_access, element_dict) redis_elements[redis_element.get_id()] = redis_element return redis_elements
def __create_elem_from_response(self, elem_response: Dict, regexp: str = None) -> Optional[Element]: """ Creates element from a response dictionary. If regexp is given, also checks if name/caption matches regular expression :param elem_response: Raw data from a platform's response for a single Element :param regexp: Regular expression to a caption-match check :return: object: New Element, if every field persist. If regexp is given, also filters element's name/caption Any other case -- None """ name = '' agent_task_id = elem_response.get('agentTaskId', None) agent_data_id = None if agent_task_id: for link in elem_response['_links']: if link['rel'] == 'agentTask': # Extract AgentData's ID agent_data_id = int( link['href'].split('agentdata/')[1].split('/')[0]) # Caption if elem_response['caption'] is not None: name = elem_response['caption'] if regexp and not re.match(regexp, name): return None elif not agent_task_id: return None # Identifier if elem_response['_links'][0]['href']: id_href = elem_response['_links'][0]['href'] else: return None # Position and size if elem_response['posX'] and elem_response['posY'] \ and elem_response['sizeX'] and elem_response['sizeY']: pos_x = elem_response['posX'] pos_y = elem_response['posY'] size_x = elem_response['sizeX'] size_y = elem_response['sizeY'] else: return None element = Element(platform_access=self.__platform_access, name=name, identifier=id_href, board=self, pos_x=pos_x, pos_y=pos_y, size_x=size_x, size_y=size_y, agent_task_id=agent_task_id, agent_data_id=agent_data_id) return element
def get_file_from_agent_and_send_to_element(self, file_url: str, filename: str, element: Element) -> bool: """ Downloads a file from agent and uploads it to an element :param file_url: a url of the file :param filename: a name of the file to display in the element :param element: the element where file should be placed :return: True if files was uploaded, False otherwise """ if not (file_url and filename and element): return False try: file_bytes = requests.get(file_url).content except Exception as e: logger.error( self.name, f'Agent can\'t download file by url {file_url}. Error {e}') return False returned_code = element.put_file(filename, file_bytes) if returned_code not in [ HTTPStatus.CREATED, AdapterStatus.CONNECTION_ABORTED ]: logger.error( self.name, f'Agent can\'t upload file to element {element.get_id()}.') return False else: return True
def _run_update_for_task(self, doc_ref: firestore.firestore.DocumentReference, task_name: str) \ -> Optional[Element]: """ Run a periodical update for agent task stored in Firestore :param doc_ref: a reference to Firestore document with info about the task :param task_name: a name of the task :return: Nothing """ doc_id = None try: task_content = self.db.get_doc_content(doc_ref) platform_access = self.__get_platform_access() board = Board(platform_access, identifier=task_content[BOARD_IDENTIFIER_KEY]) element = Element(platform_access, identifier=task_content[ELEMENT_ID_KEY], board=board) agent_task = task_content[AGENT_TASK_KEY] doc_id = self.db.get_doc_id(doc_ref) self.task_running_updaters[doc_id] = doc_ref except Exception as e: logger.exception( self.name, f'Could not perform periodical update for task {task_name}. Error: {e}' ) return None else: self._run_on_element(element=element, agent_task=agent_task, update=True, doc_id=doc_id) finally: self.db.finish_monitoring_task(doc_ref, update=True) if doc_id: self.task_running_updaters.pop(doc_id)
def _run_on_element(self, element: Element, agent_task: Dict = None, update: bool = False, doc_id: str = None) -> Optional[Element]: """ Running on a target element... :param element: own_adapter.element.Element on which the agent should run... :param agent_task: an agent_task with answers for an agent to process :param update: if an element is being updated :param doc_id: optional id of a document in Firestore to send messages from Agent :return: Target element or None if board or board.element or task are not defined """ if not element: logger.exception(self.name, 'Element was not provided') return None board = element.get_board() if not board: logger.exception(self.name, f'Board is undefined.') return None if agent_task: return self.identify_and_pass_task(element, agent_task, update) return None
def __upload_jokes(self, element: Element, query: str) -> Optional[List]: """ Uploads the metadata for the given query __it uses REST-API__ :param query: Query to find jokes about :return: Plain or None """ logger.debug(self.name, 'Privately started to upload jokes') board = element.get_board() if not board: logger.error(self.name, f'Couldn\'t get the board of element [{element}].') return None response = None try: logger.debug( self.name, f'Connecting to {self.redis_name} via REST for [{query}]') self.update_element_caption(element, query) # Compose the data to be sent to the agent. data = { REQ_QUERY_KEY: query, } # Get the result-data from Jokes-agent's REST-API. request_method = 'POST' request_flask_endpoint = '' jokes_response = self.send_request_to_agent_handler( method=request_method, url=request_flask_endpoint, params={}, data=data) # Check if we get successful response. if not jokes_response or jokes_response.status_code != HTTPStatus.OK: logger.exception( self.name, f'Error: upload jokes about {query} failed.' f' Skipped. Response: {response.status_code if response else None}', response) return None # Parse the received data. logger.debug(self.name, 'Success. Parsing the data') response_data = jokes_response.json() # Return the results. logger.info(self.name, 'Success, returning the data', response) return response_data except Exception as excpt: logger.exception( self.name, f'Error: upload of query [{query}; {year}] is failed. ' f'Skipped. Error type: {excpt}', response) return None
def on_websocket_message(self, ws: websocket.WebSocketApp, message: str) -> None: """Processes websocket messages""" message_dict = json.loads(message) content_type = message_dict['contentType'] message_type = content_type.replace('application/vnd.uberblik.', '') debug(self.name, message) if message_type == 'liveUpdateAgentTaskElementAnswersSaved+json': # Get the data from the LiveUpdate's message agent_data_id = int(message_dict['agentDataId']) agent_task_id = int(message_dict['agentTaskId']) element_id = int(message_dict['elementId']) board_id = int(message_dict['boardId']) query_id = str(message_dict['agentQueryId']) agent = self.get_agent() agent_task = get_agent_task_answers_by_id( agent.get_platform_access(), agent_data_id=agent_data_id, agent_task_id=agent_task_id, board_id=board_id, element_id=element_id) board = Board.get_board_by_id(board_id, agent.get_platform_access(), need_name=False) element = Element.get_element_by_id(element_id, agent.get_platform_access(), board) if element: # Run AgentTask on the element updated_element = self._run_on_element_default( element, query_id, agent_task) if updated_element: element = updated_element element.set_last_processing_time(datetime.datetime.now()) agent.cache_element_to_redis(element) elif message_type in [ 'liveUpdateElementPermanentlyDeleted+json', 'liveUpdateElementDeleted+json' ]: element_id = f'/{"/".join(message_dict["path"].split("/")[-4:])}' self.db.delete_old_agent_tasks(element_id) elif message_type == 'liveUpdateAgentTaskElementDeleted+json': # Get the data from the LiveUpdate's message element_id = int(message_dict['elementId']) board_id = int(message_dict['boardId']) full_element_id = f'/boards/{board_id}/elements/{element_id}' self.db.delete_old_agent_tasks(full_element_id) elif message_type == 'liveUpdateBoardDeleted+json': board_id = int(message_dict['path'].split('/')[-1]) self.db.delete_old_agent_tasks('', board_id)
def _run_on_element_and_save_task(self, element: Element, task_name: str, query_id: str, agent_task: Dict = None, start_listener: bool = False, update: bool = False, constant_monitoring: bool = False) \ -> Optional[Element]: """ Create document to communicate with agent, run on a target element :param query_id: an id of agent task query :param task_name: a name of task which is being executed :param update: if an element is being updated :param element: own_adapter.element.Element on which the agent should run :param agent_task: an agent_task with answers for an agent to process :param constant_monitoring: whether this task should run in constant monitoring mode :param start_listener: if a communication listener should be started for this task :return: Target element """ self.db.delete_old_agent_tasks(element.get_id()) doc_ref = self.db.create_new_doc_for_task( element.get_board().get_id(), element.get_id(), query_id, task_name, update_period=self.updating_time_interval, constant_monitoring=constant_monitoring, agent_task=agent_task) doc_id = self.db.get_doc_id(doc_ref) try: self.task_running_updaters[doc_id] = doc_ref if start_listener or constant_monitoring: threading.Thread( target=lambda: self.communication_handling(doc_ref), daemon=True).start() res = self._run_on_element(element, agent_task, update, doc_id) if not res: self.db.delete_doc(doc_ref) return res except Exception as e: logger.exception( self.name, f'Couldn\'t run updates on element for task: {task_name}. Error: {e}' ) finally: self.db.finish_monitoring_task(doc_ref, update=True) self.task_running_updaters.pop(doc_id)
def cache_element_to_redis(self, element: Element) -> None: """ Caches an element to Redis :param element: Element to be cached :return: Nothing """ connection = get_redis_connection() connection.hmset(f'{self.__redis_name}:elements:{element.get_id()}', element.to_dictionary())
def get_jokes(self, element: Element, agent_task: Dict) -> Optional[Element]: """ Run on element which requested jokes. :param element: own_adapter.element.Element on which Jokes-agent should run. :param agent_task: An agent task to get details from. :return: Target element or None if something gone wrong """ board = element.get_board() # Extract the topic for the joke. topic = get_answer_from_agent_task_answers(agent_task, answer_index=1) topic = str(topic[0]) if topic else '' # Put start-message on the board. start_msg = f'Time to find a joke for thee.' if topic: start_msg += f' "{topic}" you say? Let\'s see...' board.put_message(start_msg) try: # Get the jokes from the agent. uploaded_jokes = self.__upload_jokes(element=element, query=topic) if not uploaded_jokes: # No jokes for you. message = f'I could not find any jokes on "{topic}".' \ if topic \ else 'There are no more jokes in this world.' board.put_message(message) return None # Compose successful report message. message = f'"{uploaded_jokes}"' # Make a joke in a joke from time to time. if randint(-3, 10) < 0: prefix = 'I haven\'t found a thing. Just joking, here you go:\n' message = prefix + message board.put_message(message) except AttributeError as attr_err: logger.exception( self.name, f'Some attribute was incorrect while running Jokes-agent on element: {attr_err}' ) return None return element