Skip to content

Commit 3cb597a

Browse files
authored
V1 of Structured Agent & Tool logging (#379)
This establishes the following pattern of logging: - Named, constant keys present in the `extras` block can flag certain logging messages as having a particular role or location within the Agent processing pipeline. - REPL output is logging-based, using knowledge of these special fields. This avoids `print` statements and creates consistency between "what's tracked / traced" and "what the REPL can show" This permits the following follow-ups: - The Fluent Logging handler in prod can ferry this structured data to (like elastic) - User-visible logs can filter out specific items like Actions take, Thoughts thought, Tool inputs provided, etc. - The AgentContext object can provide a logger which automatically adds some of these special `extra` fields so the user doesn't need to be the wiser
1 parent b4fa569 commit 3cb597a

4 files changed

Lines changed: 158 additions & 10 deletions

File tree

src/steamship/agents/logging.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
class AgentLogging:
2+
"""These keys are for use in the `extra` field of agent logging operations. #noqa: RST203
3+
4+
For now, they are manually applied at the time of logging. In the future, the AgentContext may provide a logger
5+
which fills some automatically.
6+
7+
For example:
8+
9+
logging.info("I should use tool MakeAPicture", extra={
10+
AgentLogging.AGENT_NAME: self.name,
11+
AgentLogging.IS_AGENT_MESSAGE: True,
12+
AgentLogging.MESSAGE_TYPE: AgentLogging.THOUGHT
13+
}) # noqa: RST203
14+
15+
This provides:
16+
17+
* Structured additions to Fluent/Elastic that help with internal debugging.
18+
* Helpful output in development mode
19+
* [Eventual] User-visible logs
20+
* [Eventual] Visualiations about tool execution and ReAct reasoning
21+
22+
"""
23+
24+
AGENT_NAME = "agent_name"
25+
TOOL_NAME = "tool_name"
26+
IS_MESSAGE = "is_message"
27+
28+
MESSAGE_TYPE = "agent_message_type"
29+
THOUGHT = "thought"
30+
OBSERVATION = "observation"
31+
ACTION = "action"
32+
MESSAGE = "message"
33+
34+
MESSAGE_AUTHOR = "message_author"
35+
USER = "user"
36+
AGENT = "agent"
37+
TOOL = "tool"
38+
SYSTEM = "system"

src/steamship/agents/service/agent_service.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22

33
from steamship import SteamshipError, Task
4+
from steamship.agents.logging import AgentLogging
45
from steamship.agents.schema import Action, Agent, FinishAction
56
from steamship.agents.schema.context import AgentContext
67
from steamship.invocable import PackageService
@@ -34,12 +35,28 @@ def run_action(self, action: Action, context: AgentContext):
3435
def run_agent(self, agent: Agent, context: AgentContext):
3536
action = agent.next_action(context=context)
3637
while not isinstance(action, FinishAction):
37-
# TODO: logging?
38-
logging.info(f"running action: {action}")
38+
# TODO: Arrive at a solid design for the details of this structured log object
39+
logging.info(
40+
f"Running Tool {action.tool.name}",
41+
extra={
42+
AgentLogging.TOOL_NAME: action.tool.name,
43+
AgentLogging.IS_MESSAGE: True,
44+
AgentLogging.MESSAGE_TYPE: AgentLogging.ACTION,
45+
AgentLogging.MESSAGE_AUTHOR: AgentLogging.AGENT,
46+
},
47+
)
3948
self.run_action(action=action, context=context)
4049
action = agent.next_action(context=context)
41-
# TODO: logging?
42-
logging.info(f"next action: {action}")
50+
# TODO: Arrive at a solid design for the details of this structured log object
51+
logging.info(
52+
f"Next Tool: {action.tool.name}",
53+
extra={
54+
AgentLogging.TOOL_NAME: action.tool.name,
55+
AgentLogging.IS_MESSAGE: False,
56+
AgentLogging.MESSAGE_TYPE: AgentLogging.ACTION,
57+
AgentLogging.MESSAGE_AUTHOR: AgentLogging.AGENT,
58+
},
59+
)
4360

4461
context.completed_steps.append(action)
4562
for func in context.emit_funcs:
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import logging
2+
from logging import StreamHandler
3+
from typing import cast
4+
5+
from fluent.handler import FluentRecordFormatter
6+
7+
from steamship.agents.logging import AgentLogging
8+
9+
LOGGING_FORMAT = {
10+
"level": "%(levelname)s",
11+
"host": "%(hostname)s",
12+
"where": "%(module)s.%(filename)s.%(funcName)s:%(lineno)s",
13+
"type": "%(levelname)s",
14+
"stack_trace": "%(exc_text)s",
15+
"message_type": "%(message_type)s",
16+
"component_name": "%(component_name)s",
17+
AgentLogging.IS_MESSAGE: f"%({AgentLogging.IS_MESSAGE})s", # b doesn't work. Unsure how to make a bool
18+
AgentLogging.AGENT_NAME: f"%({AgentLogging.AGENT_NAME})s",
19+
AgentLogging.MESSAGE_AUTHOR: f"%({AgentLogging.MESSAGE_AUTHOR})s",
20+
AgentLogging.MESSAGE_TYPE: f"%({AgentLogging.MESSAGE_TYPE})s",
21+
AgentLogging.TOOL_NAME: f"%({AgentLogging.TOOL_NAME})s",
22+
}
23+
24+
25+
class DevelopmentLoggingHandler(StreamHandler):
26+
"""A logging handler for developing Steamship Agents, Tools, Packages, and Plugins locally."""
27+
28+
log_level: any
29+
log_level_with_message_type: any
30+
31+
def __init__(self, log_level: any = logging.WARN, log_level_for_messages: any = logging.INFO):
32+
StreamHandler.__init__(self)
33+
formatter = FluentRecordFormatter(LOGGING_FORMAT, fill_missing_fmt_key=True)
34+
self.setFormatter(formatter)
35+
self.log_level = log_level
36+
self.log_level_for_messages = log_level_for_messages
37+
38+
def _emit_regular(self, message_dict: dict):
39+
level = message_dict.get("level", None)
40+
message = message_dict.get("message", None)
41+
print(f"[{level}] {message}")
42+
43+
def _emit_message(self, message_dict: dict):
44+
author = message_dict.get(AgentLogging.MESSAGE_AUTHOR, "Unknown")
45+
message = message_dict.get("message", None)
46+
message_type = message_dict.get(AgentLogging.MESSAGE_TYPE, AgentLogging.MESSAGE)
47+
48+
print(f"[{author} {message_type}] {message}")
49+
50+
def emit(self, record):
51+
"""Emit the record, printing it to console out.
52+
53+
We rely on TWO logging levels for the mechanics of this LoggingHandler:
54+
55+
- One for standard logging
56+
- One for specific system Agent-related events, flagged with metadata
57+
58+
This is to permit INFO-level logging of key Agent/Tool actions without committing the user to see all
59+
INFO-level logging globally.
60+
61+
A future implementation may use a cascade of loggers attached to the AgentContext to do this more cleanly.
62+
"""
63+
message_dict = cast(dict, self.format(record))
64+
65+
# It will be returned as a string representation of a bool
66+
is_message = message_dict.get(AgentLogging.IS_MESSAGE, None) == "True"
67+
68+
if record.levelno >= self.log_level and not is_message:
69+
return self._emit_regular(message_dict)
70+
elif record.levelno >= self.log_level_for_messages and is_message:
71+
return self._emit_message(message_dict)

src/steamship/utils/repl.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,26 @@
66
from typing import Any, Dict, List, Optional, Type, cast
77

88
from steamship import Block, Steamship, Task
9+
from steamship.agents.logging import AgentLogging
910
from steamship.agents.schema import AgentContext, Tool
1011
from steamship.agents.service.agent_service import AgentService
1112
from steamship.data.workspace import SignedUrl, Workspace
13+
from steamship.invocable.dev_logging_handler import DevelopmentLoggingHandler
1214
from steamship.utils.signed_urls import upload_to_signed_url
1315

1416

1517
class SteamshipREPL(ABC):
1618
"""Base class for building REPLs that facilitate running Steamship code in the IDE."""
1719

1820
client: Steamship
21+
dev_logging_handler: DevelopmentLoggingHandler
22+
23+
def __init__(self, log_level=None):
24+
logger = logging.getLogger()
25+
logger.handlers.clear()
26+
logger.setLevel(log_level or logging.DEBUG)
27+
dev_logging_handler = DevelopmentLoggingHandler()
28+
logger.addHandler(dev_logging_handler)
1929

2030
def _make_public_url(self, block):
2131
filepath = str(uuid.uuid4())
@@ -47,18 +57,29 @@ def _make_public_url(self, block):
4757

4858
def print_blocks(self, blocks: List[Block], metadata: Dict[str, Any]):
4959
"""Print a list of blocks to console."""
60+
output = None
61+
5062
for block in blocks:
5163
if isinstance(block, dict):
5264
block = Block.parse_obj(block)
5365
if block.is_text():
54-
print(block.text)
66+
output = block.text
5567
elif block.url:
56-
print(block.url)
68+
output = block.url
5769
elif block.content_url:
58-
print(block.content_url)
70+
output = block.content_url
5971
else:
6072
url = self._make_public_url(block)
61-
print(url)
73+
output = url
74+
75+
if output:
76+
logging.info(
77+
f"{output}",
78+
extra={
79+
AgentLogging.IS_MESSAGE: True,
80+
AgentLogging.MESSAGE_AUTHOR: AgentLogging.AGENT,
81+
},
82+
)
6283

6384
@contextlib.contextmanager
6485
def temporary_workspace(self) -> Steamship:
@@ -145,8 +166,9 @@ def colored(text: str, color: str):
145166

146167
while True:
147168
input_text = input(colored(text="Input: ", color="blue")) # noqa: F821
148-
fn = getattr(agent_service, self.method)
149-
print(colored(text=f"{fn(input_text)}", color="green", force_color=True))
169+
responder = getattr(agent_service, self.method)
170+
response = responder(input_text)
171+
print(colored(text=f"{response}", color="green", force_color=True))
150172

151173
def run(self):
152174
with self.temporary_workspace() as client:

0 commit comments

Comments
 (0)