From ce36c2f0cf18de40319f941bbd4496366e2375bb Mon Sep 17 00:00:00 2001 From: hs Date: Wed, 24 Jul 2024 15:01:26 +0800 Subject: [PATCH] [feature] update the docstring in chat, worker/fronted, utils --- memory_scope/chat/cli_memory_chat.py | 20 ++++---- .../worker/frontend/fuse_rerank_worker.py | 6 +++ .../worker/frontend/print_memory_worker.py | 9 ++++ .../worker/frontend/read_message_worker.py | 7 ++- .../worker/frontend/retrieve_memory_worker.py | 14 +++++- .../worker/frontend/semantic_rank_worker.py | 14 +++--- memory_scope/utils/datetime_handler.py | 24 +++++----- memory_scope/utils/global_context.py | 4 ++ memory_scope/utils/logger.py | 9 ++-- memory_scope/utils/memory_handler.py | 48 +++++++++++++++++++ memory_scope/utils/prompt_handler.py | 6 +-- memory_scope/utils/registry.py | 12 ++++- memory_scope/utils/response_text_parser.py | 25 ++++++++-- memory_scope/utils/timer.py | 24 ++++++++++ memory_scope/utils/tool_functions.py | 39 +++++++++++---- 15 files changed, 211 insertions(+), 50 deletions(-) diff --git a/memory_scope/chat/cli_memory_chat.py b/memory_scope/chat/cli_memory_chat.py index e0442665..0f02e425 100644 --- a/memory_scope/chat/cli_memory_chat.py +++ b/memory_scope/chat/cli_memory_chat.py @@ -43,7 +43,7 @@ class CliMemoryChat(BaseMemoryChat): memory_service (str | BaseMemoryService): The memory service to be used for storing conversation history. generation_model (str | BaseModel): The model responsible for generating AI responses. stream (bool, optional): Flag indicating whether responses should be streamed. Defaults to True. - human_name (str, optional): The name assigned to the human user. Defaults to a language-specific default. + human_name (str, optional): The name assigned to the human user. Defaults to a language-specific user. assistant_name (str, optional): The name of the AI assistant. Defaults to "AI". **kwargs: Additional keyword arguments for flexibility or future extensions. @@ -98,15 +98,15 @@ class CliMemoryChat(BaseMemoryChat): def memory_service(self) -> BaseMemoryService: """ Property to access the memory service. If the service is initially set as a string, - it will be looked up in the global context's memory service dictionary, initialized, + it will be looked up in the memory service dictionary of global context, initialized, and then returned as an instance of `BaseMemoryService`. Ensures the memory service is properly started before use. Returns: - BaseMemoryService: The active memory service instance. + BaseMemoryService: An active memory service instance. Raises: - ValueError: If the memory service string reference is not found in the global context's dictionary. + ValueError: If the declaration of memory service is not found in the memory service dictionary of global context. """ if isinstance(self._memory_service, str): if self._memory_service not in G_CONTEXT.memory_service_dict: @@ -123,10 +123,10 @@ class CliMemoryChat(BaseMemoryChat): context's model dictionary. Raises: - ValueError: If the model string is not found in the global context's model dictionary. + ValueError: If the declaration of generation model is not found in the model dictionary of global context . Returns: - BaseModel: The actual generation model instance. + BaseModel: An actual generation model instance. """ if isinstance(self._generation_model, str): if self._generation_model not in G_CONTEXT.model_dict: @@ -146,12 +146,12 @@ class CliMemoryChat(BaseMemoryChat): Defaults to False. Returns: - - ModelResponse: In non-streaming mode, returns the complete AI response. + - ModelResponse: In non-streaming mode, returns a complete AI response. - ModelResponseGen: In streaming mode, returns a generator yielding AI response parts. Side Effects: - - Updates the conversation memory with the user's query and (optionally) the AI's response. - - Retrieves and includes historical messages and memory content in the conversation context. + - Updates the conversation memory with the query of user and (optionally) the response of AI. + - Retrieves and includes historical messages and memory content in the context of conversation. """ new_message: Message = Message(role=MessageRoleEnum.USER.value, role_name=self.human_name, content=query) self.memory_service.add_messages(new_message) @@ -227,7 +227,7 @@ class CliMemoryChat(BaseMemoryChat): query (str): The user's input command string. Returns: - bool: Indicates whether to continue running the CLI after processing the command. + bool: Indicates whether to continue running the CLI after processing the command. """ continue_run = True command, kwargs = self.parse_query_command(query) diff --git a/memory_scope/memory/worker/frontend/fuse_rerank_worker.py b/memory_scope/memory/worker/frontend/fuse_rerank_worker.py index ecd6a629..dc46b4ba 100644 --- a/memory_scope/memory/worker/frontend/fuse_rerank_worker.py +++ b/memory_scope/memory/worker/frontend/fuse_rerank_worker.py @@ -7,6 +7,9 @@ from memory_scope.utils.datetime_handler import DatetimeHandler class FuseRerankWorker(MemoryBaseWorker): + """ + Reranks the memory nodes by scores, types, and temporal relevance. Formats the top-K reranked nodes to print. + """ def _parse_params(self, **kwargs): self.fuse_score_threshold: float = kwargs.get("fuse_score_threshold", 0.1) @@ -16,6 +19,9 @@ class FuseRerankWorker(MemoryBaseWorker): @staticmethod def match_node_time(extract_time_dict: Dict[str, str], node: MemoryNode): + """ + Determines whether the node is relevant. + """ if extract_time_dict: match_event_flag = True for k, v in extract_time_dict.items(): diff --git a/memory_scope/memory/worker/frontend/print_memory_worker.py b/memory_scope/memory/worker/frontend/print_memory_worker.py index ed1902fd..c1f034b1 100644 --- a/memory_scope/memory/worker/frontend/print_memory_worker.py +++ b/memory_scope/memory/worker/frontend/print_memory_worker.py @@ -9,9 +9,18 @@ from memory_scope.utils.datetime_handler import DatetimeHandler class PrintMemoryWorker(MemoryBaseWorker): + """ + Formats the memories to print. + """ FILE_PATH: str = __file__ def _run(self): + """ + Executes the primary function, it involves: + 1. Fetches the memories. + 2. Formats them by 'print_template'. + 3. Set the formatted string back into the worker's context + """ # get long-term memory memory_node_list: List[MemoryNode] = self.memory_handler.get_memories(RETRIEVE_MEMORY_NODES) memory_node_list = sorted(memory_node_list, key=lambda x: x.timestamp, reverse=True) diff --git a/memory_scope/memory/worker/frontend/read_message_worker.py b/memory_scope/memory/worker/frontend/read_message_worker.py index 4d08b2f4..2841ccde 100644 --- a/memory_scope/memory/worker/frontend/read_message_worker.py +++ b/memory_scope/memory/worker/frontend/read_message_worker.py @@ -4,8 +4,13 @@ from memory_scope.memory.worker.memory_base_worker import MemoryBaseWorker class ReadMessageWorker(MemoryBaseWorker): - + """ + Fetches unmemorized chat messages. + """ def _run(self): + """ + Executes the primary function to fetch unmemorized chat messages. + """ chat_messages = [x for x in self.chat_messages if not x.memorized] if len(chat_messages) > 0 and chat_messages[-1].role == MessageRoleEnum.USER.value: chat_messages = chat_messages[:-1] diff --git a/memory_scope/memory/worker/frontend/retrieve_memory_worker.py b/memory_scope/memory/worker/frontend/retrieve_memory_worker.py index 41265600..67a45b2e 100644 --- a/memory_scope/memory/worker/frontend/retrieve_memory_worker.py +++ b/memory_scope/memory/worker/frontend/retrieve_memory_worker.py @@ -52,7 +52,7 @@ class RetrieveMemoryWorker(MemoryBaseWorker): @timer def retrieve_from_insight(self, query: str) -> List[MemoryNode]: """ - Retrieves memories marked as insights from the database based on a query, filtered by user, target, + Retrieves memories marked as insights from the store based on a query, filtered by user, target, and set to active status. Args: @@ -79,6 +79,18 @@ class RetrieveMemoryWorker(MemoryBaseWorker): @timer def retrieve_expired_memory(self, query: str) -> List[MemoryNode]: + """ + Retrieves expired memories marked as observation from the store based on a query, filtered by user, target, + and set to active status. + + Args: + query (str): The search query to match against the memories. + + Returns: + List[MemoryNode]: A list of MemoryNode objects that match the query criteria, + limited by 'retrieve_expired_top_k'. + Returns an empty list if 'retrieve_expired_top_k' is not set. + """ if not self.retrieve_expired_top_k: return [] diff --git a/memory_scope/memory/worker/frontend/semantic_rank_worker.py b/memory_scope/memory/worker/frontend/semantic_rank_worker.py index dd0dc53d..f2521f46 100644 --- a/memory_scope/memory/worker/frontend/semantic_rank_worker.py +++ b/memory_scope/memory/worker/frontend/semantic_rank_worker.py @@ -16,13 +16,13 @@ class SemanticRankWorker(MemoryBaseWorker): def _run(self): """ Executes the primary workflow of the SemanticRankWorker which includes: - - Retrieving query and timestamp from context. - - Fetching memory nodes. - - Removing duplicate nodes. - - Ranking nodes semantically. - - Assigning scores to nodes. - - Sorting nodes by score. - - Saving the ranked nodes back with logging. + - Retrieves query and timestamp from context. + - Fetches memory nodes. + - Removes duplicate nodes. + - Ranks nodes semantically. + - Assigns scores to nodes. + - Sorts nodes by score. + - Saves the ranked nodes back with logging. If no memory nodes are retrieved or if the ranking model fails, appropriate warnings are logged. diff --git a/memory_scope/utils/datetime_handler.py b/memory_scope/utils/datetime_handler.py index d386a0a5..81ea671c 100644 --- a/memory_scope/utils/datetime_handler.py +++ b/memory_scope/utils/datetime_handler.py @@ -27,7 +27,7 @@ class DatetimeHandler(object): Attributes: self._dt (datetime.datetime): The internal datetime representation of the input. - self._dt_info_dict (dict | None): A dictionary containing parsed datetime information, initialized as None. + self._dt_info_dict (dict | None): A dictionary containing parsed datetime information, defaults to None. """ if isinstance(dt, str | int | float): if isinstance(dt, str): @@ -65,7 +65,7 @@ class DatetimeHandler(object): def dt_info_dict(self): """ Property method to get the dictionary containing parsed datetime information. - If not already parsed, it triggers the parsing process using `_parse_dt_info`. + If None, initialize using `_parse_dt_info`. Returns: dict: A dictionary with parsed datetime information. @@ -77,7 +77,7 @@ class DatetimeHandler(object): @classmethod def extract_date_parts_cn(cls, input_string: str) -> dict: """ - Extracts date components from a Chinese text string into a dictionary. + Extracts various components of a date (year, month, day, etc.) from an input string based on Chinese formats. This method identifies year, month, day, weekday, and hour components within the input string based on predefined patterns. It supports relative terms like '每' (every) and @@ -125,7 +125,7 @@ class DatetimeHandler(object): text. It supports extraction of year, month name, day, 12-hour and 24-hour time formats, and weekdays. Args: - input_string (str): The string from which to extract date and time components. + input_string (str): The English text containing date and time information. Returns: dict: A dictionary containing the extracted date parts with default values of -1 where components are not @@ -212,7 +212,7 @@ class DatetimeHandler(object): Extracts various date components from the input string based on the current language context. This method dynamically selects a language-specific function to parse the input string and extract - date parts such as year, month, day, etc. If the function for the current language context does not exist, + date parts such as year, month, day, etc. If the function for current language context does not exist, a warning is logged and an empty dictionary is returned. Args: @@ -233,10 +233,10 @@ class DatetimeHandler(object): Check if the input query contains any datetime-related words based on the cn language context. Args: - query (str): The input string to check for datetime words. + query (str): The input string to check for datetime-related words. Returns: - bool: True if the query contains at least one datetime word, False otherwise. + bool: True if the query contains at least one datetime-related word, False otherwise. """ contain_datetime = False # TODO use re @@ -252,10 +252,10 @@ class DatetimeHandler(object): Check if the input query contains any datetime-related words based on the en language context. Args: - query (str): The input string to check for datetime words. + query (str): The input string to check for datetime-related words. Returns: - bool: True if the query contains at least one datetime word, False otherwise. + bool: True if the query contains at least one datetime-related word, False otherwise. """ contain_datetime = False for datetime_word in DATATIME_WORD_LIST[G_CONTEXT.language]: @@ -283,7 +283,7 @@ class DatetimeHandler(object): dt_format (str, optional): The datetime format string. Defaults to "%Y%m%d". Returns: - str: The formatted datetime string. + str: A formatted datetime string. """ return self._dt.strftime(dt_format) @@ -295,7 +295,7 @@ class DatetimeHandler(object): string_format (str): A format string where placeholders are keys from `dt_info_dict`. Returns: - str: The formatted string with datetime information inserted. + str: A formatted datetime string. """ return string_format.format(**self.dt_info_dict) @@ -305,6 +305,6 @@ class DatetimeHandler(object): Get the timestamp representation of the stored datetime. Returns: - int: The timestamp value of the datetime. + int: A timestamp value. """ return int(self._dt.timestamp()) diff --git a/memory_scope/utils/global_context.py b/memory_scope/utils/global_context.py index 7c0a3b5d..5bab0afb 100644 --- a/memory_scope/utils/global_context.py +++ b/memory_scope/utils/global_context.py @@ -10,6 +10,10 @@ from memory_scope.storage.base_monitor import BaseMonitor class GlobalContext(object): + """ + The GlobalContext class archives all configs utilized by store, monitor, services and workers. + """ + def __init__(self): self.global_config: Dict[str, Any] = {} self.worker_config: Dict[str, Dict[str, Any]] = {} diff --git a/memory_scope/utils/logger.py b/memory_scope/utils/logger.py index bad19709..9c717994 100644 --- a/memory_scope/utils/logger.py +++ b/memory_scope/utils/logger.py @@ -9,6 +9,9 @@ LOGGER_DICT = {} class Logger(logging.Logger): + """ + The `Logger` class handle the stream of information or errors in activities. + """ def __init__(self, name: str, level: int = logging.INFO, @@ -138,8 +141,8 @@ class Logger(logging.Logger): msg (str): The logged message, before formatting. args (tuple): The arguments to the log message. exc_info (tuple): Exception information or None. - func (function): The function where the logging call was made. - extra (dict): Additional information for the log record, can be None. + func (function): The function where the logging call was made. Defaults to None. + extra (dict): Additional information for the log record. Defaults to None. sinfo (str): Stack trace information or None. Returns: @@ -165,7 +168,7 @@ class Logger(logging.Logger): **kwargs: Additional keyword arguments to configure the logger. Returns: - Logger: The requested or newly created logger instance. + Logger: The requested or newly created logger instance. """ if name is None: if LOGGER_DICT: diff --git a/memory_scope/utils/memory_handler.py b/memory_scope/utils/memory_handler.py index c218fce3..80c41adf 100644 --- a/memory_scope/utils/memory_handler.py +++ b/memory_scope/utils/memory_handler.py @@ -9,8 +9,14 @@ from memory_scope.utils.logger import Logger class MemoryHandler(object): + """ + The `MemoryHandler` class manages memory nodes with memory store. + """ def __init__(self): + """ + Initializes the MemoryHandler. + """ self._memory_store: BaseMemoryStore | None = None # dict: memory_id -> MemoryNode @@ -34,10 +40,21 @@ class MemoryHandler(object): return self._memory_store def clear(self): + """ + Clear all memory nodes cached, reset the class instance. + """ self._id_memory_dict.clear() self._key_id_dict.clear() def add_memories(self, key: str, nodes: MemoryNode | List[MemoryNode], log_repeat: bool = True): + """ + Add the memories. + + Args: + key (str): The key mapping to memory nodes. + nodes (List[MemoryNode]): A single memory node or a list of memory nodes to be updated. + log_repeat (bool): Log duplicated memory node or not. + """ if key not in self._key_id_dict: return self.set_memories(key, nodes, log_repeat) @@ -55,6 +72,13 @@ class MemoryHandler(object): f"store_status={node.store_status} action_status={node.action_status}") def set_memories(self, key: str, nodes: MemoryNode | List[MemoryNode], log_repeat: bool = True): + """ + Add the memories into '_id_memory_dict' and '_key_id_dict'. + + Args: + key (str): The key mapping to memory nodes. + nodes (List[MemoryNode]): A single memory node or a list of memory nodes to be updated. + """ if nodes is None: nodes = [] elif isinstance(nodes, MemoryNode): @@ -74,6 +98,15 @@ class MemoryHandler(object): self._key_id_dict[key] = [n.memory_id for n in nodes] def get_memories(self, keys: str | List[str]) -> List[MemoryNode]: + """ + Fetch the memories by keys. + + Args: + key (str): The key mapping to memory nodes. + + Returns: + List[MemoryNode]: Memories mapped to the key. + """ memories: Dict[str, MemoryNode] = {} if isinstance(keys, str): @@ -93,6 +126,13 @@ class MemoryHandler(object): return list(memories.values()) def delete_memories(self, nodes: MemoryNode | List[MemoryNode], key: str = None): + """ + Delete the memories. + + Args: + key (str): The key mapping to memory nodes. + nodes (List[MemoryNode]): A single memory node or a list of memory nodes to be deleted. + """ if isinstance(nodes, MemoryNode): nodes = [nodes] @@ -111,6 +151,14 @@ class MemoryHandler(object): id_list.remove(_id) def update_memories(self, keys: str = "", nodes: MemoryNode | List[MemoryNode] = None): + """ + Update the memories. + + Args: + keys (str): The memories. + nodes (List[MemoryNode]): A single memory node or a list of memory nodes to be updated. + : + """ update_memories: Dict[str, MemoryNode] = {n.memory_id: n for n in self.get_memories(keys=keys)} if nodes is not None: diff --git a/memory_scope/utils/prompt_handler.py b/memory_scope/utils/prompt_handler.py index d0aec970..8fdecdeb 100644 --- a/memory_scope/utils/prompt_handler.py +++ b/memory_scope/utils/prompt_handler.py @@ -108,7 +108,7 @@ class PromptHandler(object): self._prompt_dict[key] = prompts.strip() @property - def prompt_dict(self): + def prompt_dict(self) -> dict: """ Retrieves the internal dictionary containing all prompt messages. @@ -117,7 +117,7 @@ class PromptHandler(object): """ return self._prompt_dict - def __getitem__(self, key: str): + def __getitem__(self, key: str) -> str: """ Enables accessing prompt messages using dictionary-like indexing. @@ -139,7 +139,7 @@ class PromptHandler(object): """ self._prompt_dict[key] = value - def __getattr__(self, key: str): + def __getattr__(self, key: str) -> str: """ Overrides attribute access to provide prompt messages dynamically. diff --git a/memory_scope/utils/registry.py b/memory_scope/utils/registry.py index 420ac5c3..df396306 100644 --- a/memory_scope/utils/registry.py +++ b/memory_scope/utils/registry.py @@ -26,6 +26,16 @@ class Registry(object): self.module_dict: Dict[str, Any] = {} def register(self, module_name: str = None, module: Any = None): + """ + Registers module in the registry in a single call. + + Args: + module_name (str): The name of module to be registered. + modules (List[Any] | Dict[str, Any]): The module to be registered. + + Raises: + NotImplementedError: If the input is already registered. + """ assert module is not None if module_name is None: module_name = module.__name__ @@ -60,7 +70,7 @@ class Registry(object): module_name (str): The name of the module to retrieve. Returns: - The registered module corresponding to the given name. + A registered module corresponding to the given name. Raises: AssertionError: If the specified module is not found in the registry. diff --git a/memory_scope/utils/response_text_parser.py b/memory_scope/utils/response_text_parser.py index 00e4d947..52d5b931 100644 --- a/memory_scope/utils/response_text_parser.py +++ b/memory_scope/utils/response_text_parser.py @@ -1,3 +1,4 @@ +from typing import List import re from memory_scope.constants.language_constants import NONE_WORD @@ -7,7 +8,7 @@ from memory_scope.utils.logger import Logger class ResponseTextParser(object): """ - The `ResponseTextParser` class is designed to process and parse response texts. It provides methods to extract specific + The `ResponseTextParser` class is designed to parse and process response texts. It provides methods to extract specific patterns from the text and filter out unnecessary information, while also logging the processing steps and outcomes. """ @@ -23,7 +24,16 @@ class ResponseTextParser(object): self.response_text: str = response_text.strip() # Strips leading and trailing whitespace from the response text self.logger: Logger = Logger.get_logger() # Initializes a logger instance for logging parsing activities - def parse_v1(self, prefix: str = ""): + def parse_v1(self, prefix: str = "") -> List[str]: + """ + Extract specific patterns from the text which match content within angle brackets. + + Args: + prefix (str): The prefix of log. Defaults to "". + + Returns: + Contents match the specific patterns. + """ result = [] for line in self.response_text.split("\n"): line = line.strip() @@ -35,7 +45,16 @@ class ResponseTextParser(object): self.logger.info(f"{prefix} response_text={self.response_text} result={result}", stacklevel=2) return result - def parse_v2(self, prefix: str = ""): + def parse_v2(self, prefix: str = "") -> List[str]: + """ + Extract lines which contain NONE_WORD in Chinese or English. + + Args: + prefix (str): The prefix of log. Defaults to "". + + Returns: + Contents match the specific patterns. + """ result = [] for line in self.response_text.split("\n"): line = line.strip() diff --git a/memory_scope/utils/timer.py b/memory_scope/utils/timer.py index 56f62d59..1a7cefa3 100644 --- a/memory_scope/utils/timer.py +++ b/memory_scope/utils/timer.py @@ -20,6 +20,18 @@ class Timer(object): float_precision: int = 4, **kwargs): + """ + Initializes the `Timer` instance with the provided args and sets up a logger + + Args: + name (str): The log name. + time_log_type (str): The log type. Defaults to 'End'. + use_ms (bool): Use 'ms' as the time scale or not. Defaults to True. + stack_level (int): The stack level of log. Defaults to 2. + float_precision (int): The precision of cost time. Defaults to 4. + + """ + self.name: str = name self.time_log_type: TIME_LOG_TYPE = time_log_type self.use_ms: bool = use_ms @@ -35,6 +47,9 @@ class Timer(object): self.logger = Logger.get_logger() def _set_cost(self): + """ + Accumulate the cost time. + """ self.t_end = time.time() self.cost = self.t_end - self.t_start if self.use_ms: @@ -42,6 +57,9 @@ class Timer(object): @property def cost_str(self): + """ + Represent the cost time into a formatted string. + """ self._set_cost() if self.use_ms: return f"cost={self.cost:.4f}ms" @@ -49,12 +67,18 @@ class Timer(object): return f"cost={self.cost:.4f}s" def __enter__(self, *args, **kwargs): + """ + Begin timing. + """ self.t_start = time.time() if self.time_log_type == "wrap": self.logger.info(f"----- {self.name}.begin -----") return self def __exit__(self, *args, **kwargs): + """ + End timing and print the formatted log. + """ if self.time_log_type == "none": return diff --git a/memory_scope/utils/tool_functions.py b/memory_scope/utils/tool_functions.py index d832f232..b3f5cd28 100644 --- a/memory_scope/utils/tool_functions.py +++ b/memory_scope/utils/tool_functions.py @@ -16,7 +16,17 @@ ALL_COLORS = ["red", "green", "yellow", "blue", "magenta", "cyan", "light_grey", "light_yellow", "light_blue", "light_magenta", "light_cyan", "white"] -def underscore_to_camelcase(name: str, is_first_title: bool = True): +def underscore_to_camelcase(name: str, is_first_title: bool = True) -> str: + """ + Converts a underscore_notation string to CamelCase. + + Args: + name (str): The underscore_notation string to be converted. + is_first_title (bool): Title the first word or not. Defaults to True + + Returns: + str: A CamelCase formatted string. + """ name_split = name.split("_") if is_first_title: return "".join(x.title() for x in name_split) @@ -24,7 +34,7 @@ def underscore_to_camelcase(name: str, is_first_title: bool = True): return name_split[0] + ''.join(x.title() for x in name_split[1:]) -def camelcase_to_underscore(name: str): +def camelcase_to_underscore(name: str) -> str: """ Converts a CamelCase string to underscore_notation. @@ -32,7 +42,7 @@ def camelcase_to_underscore(name: str): name (str): The CamelCase formatted string to be converted. Returns: - str: The converted string in underscore_notation. + str: A converted string in underscore_notation. """ return re.sub(r'(? str: """ - Computes the MD5 hash of a given input string. + Computes a MD5 hash of the given input string. Args: input_string (str): The string for which the MD5 hash needs to be computed. Returns: - str: The hexadecimal representation of the MD5 hash. + str: A hexadecimal MD5 hash representation. """ m = hashlib.md5() m.update(input_string.encode('utf-8')) return m.hexdigest() -def contains_keyword(text, keywords): +def contains_keyword(text, keywords) -> bool: """ Checks if the given text contains any of the specified keywords, ignoring case.