Skip to content

Commit cce68ad

Browse files
committed
change log framework and fix llm_utils.py's logs
1 parent 9eb403c commit cce68ad

File tree

3 files changed

+226
-68
lines changed

3 files changed

+226
-68
lines changed

rdagent/core/conf.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616

1717
class RDAgentSettings(BaseSettings):
1818
# TODO: (xiao) I think most of the config should be in oai.config
19+
# Log configs
20+
log_trace_path: str | None = None
21+
log_llm_chat_content: bool = True
22+
1923
use_azure: bool = True
2024
use_azure_token_provider: bool = False
2125
managed_identity_client_id: str | None = None
@@ -28,7 +32,6 @@ class RDAgentSettings(BaseSettings):
2832
prompt_cache_path: str = str(Path.cwd() / "prompt_cache.db")
2933
session_cache_folder_location: str = str(Path.cwd() / "session_cache_folder/")
3034
max_past_message_include: int = 10
31-
log_llm_chat_content: bool = True
3235

3336
# Chat configs
3437
chat_openai_api_key: str = ""

rdagent/core/log.py

Lines changed: 187 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,138 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING, Sequence
3+
import re
4+
import sys
5+
import pickle
6+
import json
7+
import inspect
8+
from typing import TYPE_CHECKING, Sequence, Literal
9+
10+
if TYPE_CHECKING:
11+
from loguru import Logger, Message, Record
412

513
from loguru import logger
14+
from abc import abstractmethod
15+
from datetime import datetime, timezone
16+
from pathlib import Path
17+
from functools import partial
618

7-
if TYPE_CHECKING:
8-
from loguru import Logger
19+
from rdagent.core.conf import RD_AGENT_SETTINGS
20+
from rdagent.core.utils import SingletonBaseClass
21+
22+
23+
def get_caller_info():
24+
# Get the current stack information
25+
stack = inspect.stack()
26+
# The second element is usually the caller's information
27+
caller_info = stack[2]
28+
frame = caller_info[0]
29+
info = {
30+
'line': caller_info.lineno,
31+
'name': frame.f_globals['__name__'], # Get the module name from the frame's globals
32+
}
33+
return info
34+
35+
36+
class Storage:
37+
"""
38+
Basic storage to support saving objects;
39+
40+
# Usage:
41+
42+
The storage has mainly two kind of users:
43+
- The logging end: you can choose any of the following method to use the object
44+
- We can use it directly with the native logging storage
45+
- We can use it with other logging tools; For example, serve as a handler for loggers
46+
- The view end:
47+
- Mainly for the subclass of `logging.base.View`
48+
- It should provide two kind of ways to provide content
49+
- offline content provision.
50+
- online content preovision.
51+
"""
52+
53+
@abstractmethod
54+
def log(self, obj: object, name: str = "", **kwargs: dict) -> str | Path | None:
55+
"""
56+
57+
Parameters
58+
----------
59+
obj : object
60+
The object for logging.
61+
name : str
62+
The name of the object. For example "a.b.c"
63+
We may log a lot of objects to a same name
64+
"""
65+
...
66+
67+
68+
class View:
69+
"""
70+
Motivation:
71+
72+
Display the content in the storage
73+
"""
74+
75+
76+
class FileStorage(Storage):
77+
"""
78+
The info are logginged to the file systems
79+
80+
TODO: describe the storage format
81+
"""
82+
83+
def __init__(self, path: str = "./log/") -> None:
84+
self.path = Path(path)
85+
self.path.mkdir(parents=True, exist_ok=True)
86+
87+
def log(self,
88+
obj: object,
89+
name: str = "",
90+
save_type: Literal["json", "text", "pkl", "short-text"] = "short-text",
91+
timestamp: datetime | None = None,
92+
split_name: bool = True,
93+
) -> Path:
94+
if timestamp is None:
95+
timestamp = datetime.now(timezone.utc)
96+
else:
97+
timestamp = timestamp.astimezone(timezone.utc)
98+
99+
cur_p = self.path
100+
if split_name:
101+
uri_l = name.split(".")
102+
for u in uri_l:
103+
cur_p = cur_p / u
104+
else:
105+
cur_p = cur_p / name
106+
cur_p.mkdir(parents=True, exist_ok=True)
107+
108+
path = cur_p / f"{timestamp.strftime('%Y-%m-%d_%H-%M-%S-%f')}.log"
109+
110+
if save_type == "json":
111+
path = path.with_suffix(".json")
112+
with path.open("w") as f:
113+
try:
114+
json.dump(obj, f)
115+
except TypeError:
116+
json.dump(json.loads(str(obj)), f)
117+
return path
118+
elif save_type == "pkl":
119+
path = path.with_suffix(".pkl")
120+
with path.open("wb") as f:
121+
pickle.dump(obj, f)
122+
return path
123+
elif save_type == "text":
124+
obj = str(obj)
125+
with path.open("w") as f:
126+
f.write(obj)
127+
return path
128+
else:
129+
obj = str(obj).strip()
130+
if obj == "":
131+
return
132+
path = cur_p / "common_logs.log"
133+
with path.open("a") as f:
134+
f.write(f"{timestamp.isoformat()}: {obj}\n\n") # add a new line to separate logs
135+
return path
9136

10137

11138
class LogColors:
@@ -54,40 +181,67 @@ def render(self, text: str, color: str = "", style: str = "") -> str:
54181
return f"{style}{text}{self.END}"
55182

56183

57-
class RDAgentLog:
58-
# logger.add(loguru_handler, level="INFO") # you can add use storage as a loguru handler
184+
def loguru2storage_handler(storage: Storage, record: Message) -> None:
185+
msg = f"{record.record['level']} | {record.record['name']}:{record.record['line']} - {RDAgentLog.remove_ansi_codes(record.record['message'])}"
186+
storage.log(msg, timestamp=record.record["time"], save_type="short-text")
187+
188+
189+
class RDAgentLog(SingletonBaseClass):
190+
191+
def __init__(self, log_trace_path: str | None = RD_AGENT_SETTINGS.log_trace_path) -> None:
192+
if log_trace_path is None:
193+
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d_%H-%M-%S-%f")
194+
log_trace_path: Path = Path.cwd() / "log" / timestamp
195+
log_trace_path.mkdir(parents=True, exist_ok=True)
196+
197+
self.storage = FileStorage(log_trace_path)
198+
199+
# add handler to save log to storage
200+
logger.add(partial(loguru2storage_handler, self.storage))
201+
202+
self.log_stream = self.LogStreamContextManager(self.storage)
203+
204+
@staticmethod
205+
def remove_ansi_codes(s: str) -> str:
206+
ansi_escape = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]')
207+
return ansi_escape.sub('', s)
208+
209+
class LogStreamContextManager:
210+
def __init__(self, storage: Storage) -> None:
211+
self.captured_logs = []
212+
self.storage = storage
213+
214+
def capture(self, message: Message) -> None:
215+
self.captured_logs.append(message.record["message"])
216+
217+
def __enter__(self):
218+
logger.remove()
219+
logger.add(sys.stderr, format=lambda x: x["message"])
220+
logger.add(self.capture)
59221

60-
def __init__(self) -> None:
61-
self.logger: Logger = logger
222+
def __exit__(self, exc_type, exc_value, traceback) -> None:
223+
logger.info('\n')
224+
logger.remove()
225+
logger.add(partial(loguru2storage_handler, self.storage))
226+
logger.info("[stream log] " + "".join(self.captured_logs))
227+
logger.add(sys.stderr)
62228

63-
def info(self, *args: Sequence, plain: bool = False) -> None:
64-
if plain:
65-
return self.plain_info(*args)
66-
for arg in args:
67-
info = f"{LogColors.WHITE}{arg}{LogColors.END}"
68-
self.logger.info(info)
69-
return None
229+
def log_objects(self, *objs: Sequence[object]) -> None:
230+
caller_info = get_caller_info()
231+
for obj in objs:
232+
logp = self.storage.log(obj, name=f"{type(obj).__module__}.{type(obj).__name__}", save_type="pkl", split_name=False)
70233

71-
def __getstate__(self) -> dict:
72-
return {}
234+
logger.patch(lambda r: r.update(caller_info)).info(f"Logging object in {logp.absolute()}")
73235

74-
def __setstate__(self, _: str) -> None:
75-
self.logger = logger
236+
def info(self, msg: str) -> None:
237+
caller_info = get_caller_info()
238+
logger.patch(lambda r: r.update(caller_info)).info(msg)
76239

77-
def plain_info(self, *args: Sequence) -> None:
78-
for arg in args:
79-
info = f"""
80-
{LogColors.YELLOW}{LogColors.BOLD}
81-
Info:{LogColors.END}{LogColors.WHITE}{arg}{LogColors.END}
82-
"""
83-
self.logger.info(info)
240+
def warning(self, msg: str) -> None:
241+
caller_info = get_caller_info()
242+
logger.patch(lambda r: r.update(caller_info)).warning(msg)
84243

85-
def warning(self, *args: Sequence) -> None:
86-
for arg in args:
87-
info = f"{LogColors.BLUE}{LogColors.BOLD}Warning:{LogColors.END}{arg}"
88-
self.logger.warning(info)
244+
def error(self, msg: str) -> None:
245+
caller_info = get_caller_info()
246+
logger.patch(lambda r: r.update(caller_info)).error(msg)
89247

90-
def error(self, *args: Sequence) -> None:
91-
for arg in args:
92-
info = f"{LogColors.RED}{LogColors.BOLD}Error:{LogColors.END}{arg}"
93-
self.logger.error(info)

0 commit comments

Comments
 (0)