55from functools import partial
66from typing import Callable , Literal
77
8- from litellm import completion
8+ from litellm import completion , token_counter
99from litellm .types .utils import Message , ModelResponse
1010from PIL import Image
1111from 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 = """
4647You are an expert AI Agent trained to assist users with complex web tasks.
4748Your role is to understand the goal, perform actions until the goal is accomplished and respond in a helpful and accurate manner.
4849Keep 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 = """
5252Think along the following lines:
53531. Summarize the last observation and describe the visible changes in the state.
54542. Evaluate action success, explain impact on task and next steps.
55553. If you see any errors in the last observation, think about it. If there is no error, just move on.
56564. 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
6170class 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
172222class 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