You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
swarms/swarms/utils/loggers.py

543 lines
16 KiB

"""Logging modules"""
import json
import logging
import os
import random
import re
import time
from logging import LogRecord
from typing import Any
from colorama import Fore, Style
from swarms.utils.apa import Action, ToolCallStatus
# from autogpt.speech import say_text
class JsonFileHandler(logging.FileHandler):
def __init__(
self, filename, mode="a", encoding=None, delay=False
):
"""
Initializes a new instance of the class with the specified file name, mode, encoding, and delay settings.
Parameters:
filename (str): The name of the file to be opened.
mode (str, optional): The mode in which the file is opened. Defaults to "a" (append).
encoding (str, optional): The encoding used to read or write the file. Defaults to None.
delay (bool, optional): If True, the file opening is delayed until the first IO operation. Defaults to False.
Returns:
None
"""
super().__init__(filename, mode, encoding, delay)
def emit(self, record):
"""
Writes the formatted log record to a JSON file.
Parameters:
record (LogRecord): The log record to be emitted.
Returns:
None
"""
json_data = json.loads(self.format(record))
with open(self.baseFilename, "w", encoding="utf-8") as f:
json.dump(json_data, f, ensure_ascii=False, indent=4)
class JsonFormatter(logging.Formatter):
def format(self, record):
"""
Format the given record and return the message.
Args:
record (object): The log record to be formatted.
Returns:
str: The formatted message from the record.
"""
return record.msg
class Logger:
"""
Logger that handle titles in different colors.
Outputs logs in console, activity.log, and errors.log
For console handler: simulates typing
"""
def __init__(self):
"""
Initializes the class and sets up the logging configuration.
Args:
None
Returns:
None
"""
# create log directory if it doesn't exist
this_files_dir_path = os.path.dirname(__file__)
log_dir = os.path.join(this_files_dir_path, "../logs")
if not os.path.exists(log_dir):
os.makedirs(log_dir)
log_file = "activity.log"
error_file = "error.log"
console_formatter = AutoGptFormatter(
"%(title_color)s %(message)s"
)
# Create a handler for console which simulate typing
self.typing_console_handler = TypingConsoleHandler()
# self.typing_console_handler = ConsoleHandler()
self.typing_console_handler.setLevel(logging.INFO)
self.typing_console_handler.setFormatter(console_formatter)
# Create a handler for console without typing simulation
self.console_handler = ConsoleHandler()
self.console_handler.setLevel(logging.DEBUG)
self.console_handler.setFormatter(console_formatter)
# Info handler in activity.log
self.file_handler = logging.FileHandler(
os.path.join(log_dir, log_file), "a", "utf-8"
)
self.file_handler.setLevel(logging.DEBUG)
info_formatter = AutoGptFormatter(
"%(asctime)s %(levelname)s %(title)s %(message_no_color)s"
)
self.file_handler.setFormatter(info_formatter)
# Error handler error.log
error_handler = logging.FileHandler(
os.path.join(log_dir, error_file), "a", "utf-8"
)
error_handler.setLevel(logging.ERROR)
error_formatter = AutoGptFormatter(
"%(asctime)s %(levelname)s"
" %(module)s:%(funcName)s:%(lineno)d %(title)s"
" %(message_no_color)s"
)
error_handler.setFormatter(error_formatter)
self.typing_logger = logging.getLogger("TYPER")
self.typing_logger.addHandler(self.typing_console_handler)
# self.typing_logger.addHandler(self.console_handler)
self.typing_logger.addHandler(self.file_handler)
self.typing_logger.addHandler(error_handler)
self.typing_logger.setLevel(logging.DEBUG)
self.logger = logging.getLogger("LOGGER")
self.logger.addHandler(self.console_handler)
self.logger.addHandler(self.file_handler)
self.logger.addHandler(error_handler)
self.logger.setLevel(logging.DEBUG)
self.json_logger = logging.getLogger("JSON_LOGGER")
self.json_logger.addHandler(self.file_handler)
self.json_logger.addHandler(error_handler)
self.json_logger.setLevel(logging.DEBUG)
self.speak_mode = False
self.chat_plugins = []
def typewriter_log(
self,
title="",
title_color="",
content="",
speak_text=False,
level=logging.INFO,
):
"""
Logs a message to the typewriter.
Args:
title (str, optional): The title of the log message. Defaults to "".
title_color (str, optional): The color of the title. Defaults to "".
content (str or list, optional): The content of the log message. Defaults to "".
speak_text (bool, optional): Whether to speak the log message. Defaults to False.
level (int, optional): The logging level of the message. Defaults to logging.INFO.
Returns:
None
"""
for plugin in self.chat_plugins:
plugin.report(f"{title}. {content}")
if content:
if isinstance(content, list):
content = " ".join(content)
else:
content = ""
self.typing_logger.log(
level,
content,
extra={"title": title, "color": title_color},
)
def debug(
self,
message,
title="",
title_color="",
):
"""
Logs a debug message.
Args:
message (str): The debug message to log.
title (str, optional): The title of the log message. Defaults to "".
title_color (str, optional): The color of the log message title. Defaults to "".
Returns:
None
"""
self._log(title, title_color, message, logging.DEBUG)
def info(
self,
message,
title="",
title_color="",
):
"""
Logs an informational message.
Args:
message (str): The message to be logged.
title (str, optional): The title of the log message. Defaults to "".
title_color (str, optional): The color of the log title. Defaults to "".
Returns:
None
"""
self._log(title, title_color, message, logging.INFO)
def warn(
self,
message,
title="",
title_color="",
):
"""
Logs a warning message.
Args:
message (str): The warning message.
title (str, optional): The title of the warning message. Defaults to "".
title_color (str, optional): The color of the title. Defaults to "".
"""
self._log(title, title_color, message, logging.WARN)
def error(self, title, message=""):
"""
Logs an error message with the given title and optional message.
Parameters:
title (str): The title of the error message.
message (str, optional): The optional additional message for the error. Defaults to an empty string.
"""
self._log(title, Fore.RED, message, logging.ERROR)
def _log(
self,
title: str = "",
title_color: str = "",
message: str = "",
level=logging.INFO,
):
"""
Logs a message with the given title and message at the specified log level.
Parameters:
title (str): The title of the log message. Defaults to an empty string.
title_color (str): The color of the log message title. Defaults to an empty string.
message (str): The log message. Defaults to an empty string.
level (int): The log level. Defaults to logging.INFO.
Returns:
None
"""
if message:
if isinstance(message, list):
message = " ".join(message)
self.logger.log(
level,
message,
extra={"title": str(title), "color": str(title_color)},
)
def set_level(self, level):
"""
Set the level of the logger and the typing_logger.
Args:
level: The level to set the logger to.
Returns:
None
"""
self.logger.setLevel(level)
self.typing_logger.setLevel(level)
def double_check(self, additionalText=None):
"""
A function that performs a double check on the configuration.
Parameters:
additionalText (str, optional): Additional text to be included in the double check message.
Returns:
None
"""
if not additionalText:
additionalText = (
"Please ensure you've setup and configured everything"
" correctly. Read"
" https://github.com/Torantulino/Auto-GPT#readme to"
" double check. You can also create a github issue or"
" join the discord and ask there!"
)
self.typewriter_log(
"DOUBLE CHECK CONFIGURATION", Fore.YELLOW, additionalText
)
def log_json(self, data: Any, file_name: str) -> None:
"""
Logs the given JSON data to a specified file.
Args:
data (Any): The JSON data to be logged.
file_name (str): The name of the file to log the data to.
Returns:
None: This function does not return anything.
"""
# Define log directory
this_files_dir_path = os.path.dirname(__file__)
log_dir = os.path.join(this_files_dir_path, "../logs")
# Create a handler for JSON files
json_file_path = os.path.join(log_dir, file_name)
json_data_handler = JsonFileHandler(json_file_path)
json_data_handler.setFormatter(JsonFormatter())
# Log the JSON data using the custom file handler
self.json_logger.addHandler(json_data_handler)
self.json_logger.debug(data)
self.json_logger.removeHandler(json_data_handler)
def get_log_directory(self):
"""
Returns the absolute path to the log directory.
Returns:
str: The absolute path to the log directory.
"""
this_files_dir_path = os.path.dirname(__file__)
log_dir = os.path.join(this_files_dir_path, "../logs")
return os.path.abspath(log_dir)
"""
Output stream to console using simulated typing
"""
class TypingConsoleHandler(logging.StreamHandler):
def emit(self, record):
"""
Emit a log record to the console with simulated typing effect.
Args:
record (LogRecord): The log record to be emitted.
Returns:
None
Raises:
Exception: If an error occurs while emitting the log record.
"""
min_typing_speed = 0.05
max_typing_speed = 0.10
# min_typing_speed = 0.005
# max_typing_speed = 0.010
msg = self.format(record)
try:
# replace enter & indent with other symbols
transfer_enter = "<ENTER>"
msg_transfered = str(msg).replace("\n", transfer_enter)
transfer_space = "<4SPACE>"
msg_transfered = str(msg_transfered).replace(
" ", transfer_space
)
words = msg_transfered.split()
words = [
word.replace(transfer_enter, "\n") for word in words
]
words = [
word.replace(transfer_space, " ") for word in words
]
for i, word in enumerate(words):
print(word, end="", flush=True)
if i < len(words) - 1:
print(" ", end="", flush=True)
typing_speed = random.uniform(
min_typing_speed, max_typing_speed
)
time.sleep(typing_speed)
# type faster after each word
min_typing_speed = min_typing_speed * 0.95
max_typing_speed = max_typing_speed * 0.95
print()
except Exception:
self.handleError(record)
class ConsoleHandler(logging.StreamHandler):
def emit(self, record) -> None:
"""
Emit the log record.
Args:
record (logging.LogRecord): The log record to emit.
Returns:
None: This function does not return anything.
"""
msg = self.format(record)
try:
print(msg)
except Exception:
self.handleError(record)
class AutoGptFormatter(logging.Formatter):
"""
Allows to handle custom placeholders 'title_color' and 'message_no_color'.
To use this formatter, make sure to pass 'color', 'title' as log extras.
"""
def format(self, record: LogRecord) -> str:
"""
Formats a log record into a string representation.
Args:
record (LogRecord): The log record to be formatted.
Returns:
str: The formatted log record as a string.
"""
if hasattr(record, "color"):
record.title_color = (
getattr(record, "color")
+ getattr(record, "title", "")
+ " "
+ Style.RESET_ALL
)
else:
record.title_color = getattr(record, "title", "")
# Add this line to set 'title' to an empty string if it doesn't exist
record.title = getattr(record, "title", "")
if hasattr(record, "msg"):
record.message_no_color = remove_color_codes(
getattr(record, "msg")
)
else:
record.message_no_color = ""
return super().format(record)
def remove_color_codes(s: str) -> str:
"""
Removes color codes from a given string.
Args:
s (str): The string from which to remove color codes.
Returns:
str: The string with color codes removed.
"""
ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
return ansi_escape.sub("", s)
logger = Logger()
def print_action_base(action: Action):
"""
Print the different properties of an Action object.
Parameters:
action (Action): The Action object to print.
Returns:
None
"""
if action.content != "":
logger.typewriter_log(
"content:", Fore.YELLOW, f"{action.content}"
)
logger.typewriter_log(
"Thought:", Fore.YELLOW, f"{action.thought}"
)
if len(action.plan) > 0:
logger.typewriter_log(
"Plan:",
Fore.YELLOW,
)
for line in action.plan:
line = line.lstrip("- ")
logger.typewriter_log("- ", Fore.GREEN, line.strip())
logger.typewriter_log(
"Criticism:", Fore.YELLOW, f"{action.criticism}"
)
def print_action_tool(action: Action):
"""
Prints the details of an action tool.
Args:
action (Action): The action object containing the tool details.
Returns:
None
"""
logger.typewriter_log("Tool:", Fore.BLUE, f"{action.tool_name}")
logger.typewriter_log(
"Tool Input:", Fore.BLUE, f"{action.tool_input}"
)
output = (
action.tool_output if action.tool_output != "" else "None"
)
logger.typewriter_log("Tool Output:", Fore.BLUE, f"{output}")
color = Fore.RED
if action.tool_output_status == ToolCallStatus.ToolCallSuccess:
color = Fore.GREEN
elif (
action.tool_output_status == ToolCallStatus.InputCannotParsed
):
color = Fore.YELLOW
logger.typewriter_log(
"Tool Call Status:",
Fore.BLUE,
f"{color}{action.tool_output_status.name}{Style.RESET_ALL}",
)