Skip to content

Commit

Permalink
Merge pull request EmergenceAI#88 from EmergenceAI/logging_to_files_o…
Browse files Browse the repository at this point in the history
…ptional_and_structured_logging

structured logging and optional file logging
  • Loading branch information
teaxio authored Jul 30, 2024
2 parents 47df964 + 4030836 commit cbeb367
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 33 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ curl --location 'http://127.0.0.1:8000/execute_task' \
}'
```

### Additional environment variables
Agent-E has a few more env variables that can be added to `.env` or whichever environment you are using.

`SAVE_CHAT_LOGS_TO_FILE`: true | false (Default: `true`) Indicates whether to save chat logs, for planner and nested chat, into files or log them to stdout
`LOG_MESSAGES_FORMAT`: json | text (Default: `text`) Whether to using structured logging or text logging. If text is used, json objects will not be output. This will mainly be used for chat logs, so if `SAVE_CHAT_LOGS_TO_FILE` is set to `true`, then setting this to `text` will be fine.


## Demos

Expand Down
22 changes: 14 additions & 8 deletions ae/core/autogen_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,32 +40,35 @@ class AutogenWrapper:
"""

def __init__(self, max_chat_round: int = 1000):
def __init__(self, save_chat_logs_to_files: bool = True, max_chat_round: int = 1000):
self.number_of_rounds = max_chat_round

self.agents_map: dict[str, UserProxyAgent_SequentialFunctionExecution | autogen.AssistantAgent | autogen.ConversableAgent ] | None = None

self.config_list: list[dict[str, str]] | None = None
self.chat_logs_dir: str = SOURCE_LOG_FOLDER_PATH
self.save_chat_logs_to_files = save_chat_logs_to_files

@classmethod
async def create(cls, agents_needed: list[str] | None = None, max_chat_round: int = 1000):
async def create(cls, agents_needed: list[str] | None = None, save_chat_logs_to_files: bool = True, max_chat_round: int = 1000):
"""
Create an instance of AutogenWrapper.
Args:
agents_needed (list[str], optional): The list of agents needed. If None, then ["user", "browser_nav_executor", "planner_agent", "browser_nav_agent"] will be used.
save_chat_logs_to_files (bool, optional): Whether to save chat logs to files. Defaults to True.
max_chat_round (int, optional): The maximum number of chat rounds. Defaults to 50.
Returns:
AutogenWrapper: An instance of AutogenWrapper.
"""
print(f">>> Creating AutogenWrapper with {agents_needed} and {max_chat_round} rounds.")
print(f">>> Creating AutogenWrapper with {agents_needed} and {max_chat_round} rounds. Save chat logs to files: {save_chat_logs_to_files}")
if agents_needed is None:
agents_needed = ["user", "browser_nav_executor", "planner_agent", "browser_nav_agent"]
# Create an instance of cls
self = cls(max_chat_round)
self = cls(save_chat_logs_to_files=save_chat_logs_to_files, max_chat_round=max_chat_round)

load_dotenv()
os.environ["AUTOGEN_USE_DOCKER"] = "False"

Expand Down Expand Up @@ -185,10 +188,13 @@ def set_chat_logs_dir(self, chat_logs_dir: str):


def __save_chat_log(self, chat_log: list[dict[str, Any]]):
chat_logs_file = os.path.join(self.get_chat_logs_dir() or "", f"nested_chat_log_{str(time_ns())}.json")
# Save the chat log to a file
with open(chat_logs_file, "w") as file:
json.dump(chat_log, file, indent=4)
if not self.save_chat_logs_to_files:
logger.info("Nested chat logs", extra={"nested_chat_log": chat_log})
else:
chat_logs_file = os.path.join(self.get_chat_logs_dir() or "", f"nested_chat_log_{str(time_ns())}.json")
# Save the chat log to a file
with open(chat_logs_file, "w") as file:
json.dump(chat_log, file, indent=4)


async def __initialize_agents(self, agents_needed: list[str]):
Expand Down
18 changes: 14 additions & 4 deletions ae/core/system_orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
import os
import time

from dotenv import load_dotenv

import ae.core.playwright_manager as browserManager
from ae.config import SOURCE_LOG_FOLDER_PATH
from ae.core.autogen_wrapper import AutogenWrapper
from ae.utils.cli_helper import async_input # type: ignore
from ae.utils.formatting_helper import str_to_bool
from ae.utils.http_helper import make_post_request
from ae.utils.logger import logger

Expand All @@ -33,12 +36,16 @@ def __init__(self, agent_scenario:str="user,planner_agent,browser_nav_agent,brow
agent_scenario (str, optional): The agent scenario to use for command processing. Defaults to "user_proxy,browser_nav_agent".
input_mode (str, optional): The input mode of the system. Defaults to "GUI_ONLY".
"""
load_dotenv()

self.agent_scenario = agent_scenario
self.input_mode = input_mode
self.browser_manager = None
self.autogen_wrapper = None
self.is_running = False

self.save_chat_logs_to_files = str_to_bool(os.getenv('SAVE_CHAT_LOGS_TO_FILE', True))

if os.getenv('ORCHESTRATOR_API_KEY', None) is not None and os.getenv('ORCHESTRATOR_GATEWAY', None) is not None:
self.__populate_orchestrator_info()
logger.info(f"Orchestrator endpoint: {self.orchestrator_endpoint}")
Expand Down Expand Up @@ -74,7 +81,7 @@ async def initialize(self):
"""
Initializes the components required for the system's operation, including the Autogen wrapper and the Playwright manager.
"""
self.autogen_wrapper = await AutogenWrapper.create(agents_needed=self.agent_names)
self.autogen_wrapper = await AutogenWrapper.create(agents_needed=self.agent_names, save_chat_logs_to_files=self.save_chat_logs_to_files)

self.browser_manager = browserManager.PlaywrightManager(gui_input_mode=self.input_mode == "GUI_ONLY")
await self.browser_manager.async_initialize()
Expand Down Expand Up @@ -179,9 +186,12 @@ async def save_chat_messages(self):
"""
messages = self.autogen_wrapper.agents_map[self.browser_agent_name].chat_messages # type: ignore
messages_str_keys = {str(key): value for key, value in messages.items()} # type: ignore
with open(os.path.join(SOURCE_LOG_FOLDER_PATH, 'chat_messages.json'), 'w', encoding='utf-8') as f:
json.dump(messages_str_keys, f, ensure_ascii=False, indent=4)
logger.debug("Chat messages saved")
if self.save_chat_logs_to_files:
with open(os.path.join(SOURCE_LOG_FOLDER_PATH, 'chat_messages.json'), 'w', encoding='utf-8') as f:
json.dump(messages_str_keys, f, ensure_ascii=False, indent=4)
logger.debug("Chat messages saved")
else:
logger.info("Planner chat log: ", extra={"planner_chat_log": messages_str_keys}) # type: ignore

async def wait_for_exit(self):
"""
Expand Down
15 changes: 15 additions & 0 deletions ae/utils/formatting_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

def str_to_bool(s: str | bool) -> bool:
"""
Convert a string representation of truth to True or False.
Parameters:
s (str | bool): The string to convert, or a boolean.
Returns:
bool: True if the string represents a truth value, False otherwise.
"""
if isinstance(s, bool):
return s
return s.lower() in ['true', '1', 't', 'y', 'yes']

70 changes: 50 additions & 20 deletions ae/utils/logger.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,59 @@
import logging
import os

from dotenv import load_dotenv
from pythonjsonlogger import jsonlogger

# Load environment variables from a .env file
load_dotenv()

logger = logging.getLogger(__name__)
'''logging.basicConfig(
level=logging.DEBUG, # change level here or use set_log_level() to change it
format="[%(asctime)s] %(levelname)s {%(filename)s:%(lineno)d} - %(message)s", filename='app.log', filemode='a'
)'''
logging.basicConfig(level=logging.INFO)
logging.getLogger("httpcore").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("matplotlib.pyplot").setLevel(logging.WARNING)
logging.getLogger("PIL.PngImagePlugin").setLevel(logging.WARNING)
logging.getLogger("PIL.Image").setLevel(logging.WARNING)

def set_log_level(level: str | int) -> None:
# Custom function to configure the logger
def configure_logger(level: str = "INFO") -> None:
log_format = os.getenv("LOG_MESSAGES_FORMAT", "text").lower()

logger.setLevel(level.upper())

# Create a handler for logging
handler = logging.StreamHandler()

if log_format == "json":
# JSON format
formatter = jsonlogger.JsonFormatter(
fmt='%(asctime)s %(name)s %(levelname)s %(message)s %(filename)s %(lineno)d',
datefmt='%Y-%m-%d %H:%M:%S'
)
else:
# Text format
formatter = logging.Formatter(
fmt='[%(asctime)s] %(levelname)s {%(filename)s:%(lineno)d} - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)

handler.setFormatter(formatter)
logger.handlers = [] # Clear existing handlers
logger.addHandler(handler)

# Call the configure logger function to set up the logger initially
configure_logger(level="INFO")

# Function to set log level
def set_log_level(level: str) -> None:
"""
Set the log level for the logger.
Parameters:
- level (Union[str, int]): A string or logging level such as 'debug', 'info', 'warning', 'error', or 'critical', or the corresponding logging constants like logging.DEBUG, logging.INFO, etc.
- level (str): A logging level such as 'debug', 'info', 'warning', 'error', or 'critical'.
"""
if isinstance(level, str):
level = level.upper()
numeric_level = getattr(logging, level, None)
if not isinstance(numeric_level, int):
raise ValueError(f'Invalid log level: {level}')
logger.setLevel(numeric_level)
else:
logger.setLevel(level)
configure_logger(level)

# Set default log levels for other libraries
logging.getLogger("httpcore").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("matplotlib.pyplot").setLevel(logging.WARNING)
logging.getLogger("PIL.PngImagePlugin").setLevel(logging.WARNING)
logging.getLogger("PIL.Image").setLevel(logging.WARNING)

# Re-export the logger for ease of use
__all__ = ["logger", "set_log_level"]
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ dependencies = [
"tabulate==0.9.0",
"nest-asyncio==1.6.0",
"fastapi==0.111.1",
"uvicorn==0.30.3"
"uvicorn==0.30.3",
"python-json-logger==2.0.7"
]

[project.optional-dependencies]
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ python-dotenv==1.0.0
# via
# pyautogen
# uvicorn
python-json-logger==2.0.7
python-multipart==0.0.9
# via fastapi
pyyaml==6.0.1
Expand Down

0 comments on commit cbeb367

Please sign in to comment.