Skip to content

Commit 6a1caf2

Browse files
committed
history compaction
1 parent 3d88daf commit 6a1caf2

File tree

1 file changed

+68
-17
lines changed

1 file changed

+68
-17
lines changed

src/agentlab/agents/react_toolcall_agent.py

Lines changed: 68 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from functools import partial
66
from typing import Callable, Literal
77

8-
from litellm import completion
8+
from litellm import completion, token_counter
99
from litellm.types.utils import Message, ModelResponse
1010
from PIL import Image
1111
from termcolor import colored
@@ -42,29 +42,43 @@ class AgentConfig:
4242
use_axtree: bool = False
4343
use_screenshot: bool = True
4444
max_actions: int = 10
45+
max_history_tokens: int = 120000
4546
system_prompt: str = """
4647
You are an expert AI Agent trained to assist users with complex web tasks.
4748
Your role is to understand the goal, perform actions until the goal is accomplished and respond in a helpful and accurate manner.
4849
Keep your replies brief, concise, direct and on topic. Prioritize clarity and avoid over-elaboration.
49-
Do not express emotions or opinions.
50-
"""
50+
Do not express emotions or opinions."""
5151
guidance: str = """
5252
Think along the following lines:
5353
1. Summarize the last observation and describe the visible changes in the state.
5454
2. Evaluate action success, explain impact on task and next steps.
5555
3. If you see any errors in the last observation, think about it. If there is no error, just move on.
5656
4. List next steps to move towards the goal and propose next immediate action.
57-
Then produce the single function call that performs the proposed action. If the task is complete, produce the final step.
58-
"""
57+
Then produce the single function call that performs the proposed action. If the task is complete, produce the final step."""
58+
summarize_system_prompt: str = """
59+
You are a helpful assistant that summarizes conversation history. Following messages is the history to summarize:"""
60+
summarize_prompt: str = """
61+
Summarize the presented agent interaction history concisely.
62+
Focus on:
63+
- The original goal
64+
- Key actions taken and their outcomes
65+
- Important errors or obstacles encountered
66+
- Current progress toward the goal
67+
Provide a concise summary that preserves all information needed to continue the task."""
5968

6069

6170
class ReactToolCallAgent:
6271
def __init__(
63-
self, action_set: ToolsActionSet, llm: Callable[..., ModelResponse], config: AgentConfig
72+
self,
73+
action_set: ToolsActionSet,
74+
llm: Callable[..., ModelResponse],
75+
token_counter: Callable[..., int],
76+
config: AgentConfig,
6477
):
6578
self.action_set = action_set
6679
self.history: list[dict | Message] = [{"role": "system", "content": config.system_prompt}]
6780
self.llm = llm
81+
self.token_counter = token_counter
6882
self.config = config
6983
self.last_tool_call_id: str = ""
7084

@@ -113,14 +127,12 @@ def obs_to_messages(self, obs: dict) -> list[dict]:
113127
return messages
114128

115129
def get_action(self, obs: dict) -> tuple[ToolCall, dict]:
116-
actions_count = len(
117-
[msg for msg in self.history if isinstance(msg, Message) and msg.tool_calls]
118-
)
119-
if actions_count >= self.config.max_actions:
130+
if self.max_actions_reached():
120131
logger.warning("Max actions reached, stopping agent.")
121-
stop_action = ToolCall(name="final_step")
122-
return stop_action, {}
132+
return ToolCall(name="final_step"), {}
133+
123134
self.history += self.obs_to_messages(self.obs_preprocessor(obs))
135+
self.maybe_compact_history()
124136
tools = [tool.model_dump() for tool in self.action_set.actions]
125137
messages = self.history + [{"role": "user", "content": self.config.guidance}]
126138

@@ -136,21 +148,23 @@ def get_action(self, obs: dict) -> tuple[ToolCall, dict]:
136148
self.history.append(message)
137149
thoughts = self.thoughts_from_message(message)
138150
action = self.action_from_message(message)
139-
return action, {"think": thoughts}
151+
return action, {"think": thoughts, "chat_messages": self.history}
152+
153+
def max_actions_reached(self) -> bool:
154+
prev_actions = [msg for msg in self.history if isinstance(msg, Message) and msg.tool_calls]
155+
return len(prev_actions) >= self.config.max_actions
140156

141157
def thoughts_from_message(self, message: Message) -> str:
142158
thoughts = []
143159
if reasoning := message.get("reasoning_content"):
144-
logger.info(colored(f"LLM reasoning:\n{reasoning}", "yellow"))
145160
thoughts.append(reasoning)
146161
if blocks := message.get("thinking_blocks"):
147162
for block in blocks:
148163
if thinking := getattr(block, "content", None) or getattr(block, "thinking", None):
149-
logger.info(colored(f"LLM thinking block:\n{thinking}", "yellow"))
150164
thoughts.append(thinking)
151165
if message.content:
152-
logger.info(colored(f"LLM text output:\n{message.content}", "cyan"))
153166
thoughts.append(message.content)
167+
logger.info(colored(f"LLM thoughts: {thoughts}", "cyan"))
154168
return "\n\n".join(thoughts)
155169

156170
def action_from_message(self, message: Message) -> ToolCall:
@@ -167,6 +181,42 @@ def action_from_message(self, message: Message) -> ToolCall:
167181
raise ValueError(f"No tool call found in LLM response: {message}")
168182
return action
169183

184+
def maybe_compact_history(self):
185+
history_tokens = self.token_counter(messages=self.history)
186+
if len(history_tokens) > self.config.max_history_tokens:
187+
logger.info("Compacting history due to length.")
188+
self.compact_history()
189+
short_history_tokens = self.token_counter(messages=self.history)
190+
logger.info(
191+
f"Compacted history from {history_tokens} to {short_history_tokens} tokens."
192+
)
193+
194+
def compact_history(self):
195+
"""
196+
Compact the history by summarizing the first half of messages with the LLM.
197+
Updates self.history in place by replacing the first half with the summary message.
198+
"""
199+
system_msg = self.history[0]
200+
rest = self.history[1:]
201+
midpoint = len(rest) // 2
202+
messages = [
203+
{"role": "system", "content": self.config.summarize_system_prompt},
204+
*rest[:midpoint],
205+
{"role": "user", "content": self.config.summarize_prompt},
206+
]
207+
208+
try:
209+
response = self.llm(messages=messages, tool_choice="none")
210+
summary = response.choices[0].message.content # type: ignore
211+
except Exception as e:
212+
logger.exception(f"Error compacting history: {e}")
213+
raise
214+
215+
logger.info(colored(f"Compacted {midpoint} messages into summary:\n{summary}", "cyan"))
216+
# Rebuild history: system + summary + remaining messages
217+
summary_message = {"role": "user", "content": f"## Previous Interaction :\n{summary}"}
218+
self.history = [system_msg, summary_message, *rest[midpoint:]]
219+
170220

171221
@dataclass
172222
class ReactToolCallAgentArgs(AgentArgs):
@@ -175,5 +225,6 @@ class ReactToolCallAgentArgs(AgentArgs):
175225

176226
def make_agent(self, actions: list[ToolSpec]) -> ReactToolCallAgent:
177227
llm = self.llm_args.make_model()
228+
counter = partial(token_counter, model=self.llm_args.model_name)
178229
action_set = ToolsActionSet(actions=actions)
179-
return ReactToolCallAgent(action_set=action_set, llm=llm, config=self.config)
230+
return ReactToolCallAgent(action_set, llm, counter, self.config)

0 commit comments

Comments
 (0)