Selaa lähdekoodia

Small refactoring of obs truncation (#2701)

* refactor truncate_content a bit to be usable by all agents

* adjust doc
Engel Nyst 1 vuosi sitten
vanhempi
sitoutus
e24c52d060

+ 4 - 17
agenthub/codeact_agent/codeact_agent.py

@@ -21,6 +21,7 @@ from opendevin.events.observation import (
     CmdOutputObservation,
     IPythonRunCellObservation,
 )
+from opendevin.events.serialization.event import truncate_content
 from opendevin.llm.llm import LLM
 from opendevin.runtime.plugins import (
     AgentSkillsRequirement,
@@ -60,7 +61,7 @@ def get_action_message(action: Action) -> dict[str, str] | None:
 
 def get_observation_message(obs) -> dict[str, str] | None:
     if isinstance(obs, CmdOutputObservation):
-        content = 'OBSERVATION:\n' + truncate_observation(obs.content)
+        content = 'OBSERVATION:\n' + truncate_content(obs.content)
         content += (
             f'\n[Command {obs.command_id} finished with exit code {obs.exit_code}]'
         )
@@ -75,28 +76,14 @@ def get_observation_message(obs) -> dict[str, str] | None:
                     '![image](data:image/png;base64, ...) already displayed to user'
                 )
         content = '\n'.join(splitted)
-        content = truncate_observation(content)
+        content = truncate_content(content)
         return {'role': 'user', 'content': content}
     elif isinstance(obs, AgentDelegateObservation):
-        content = 'OBSERVATION:\n' + truncate_observation(str(obs.outputs))
+        content = 'OBSERVATION:\n' + truncate_content(str(obs.outputs))
         return {'role': 'user', 'content': content}
     return None
 
 
-def truncate_observation(observation: str, max_chars: int = 10_000) -> str:
-    """
-    Truncate the middle of the observation if it is too long.
-    """
-    if len(observation) <= max_chars:
-        return observation
-    half = max_chars // 2
-    return (
-        observation[:half]
-        + '\n[... Observation truncated due to length ...]\n'
-        + observation[-half:]
-    )
-
-
 # FIXME: We can tweak these two settings to create MicroAgents specialized toward different area
 def get_system_message() -> str:
     if ENABLE_GITHUB:

+ 3 - 16
agenthub/codeact_swe_agent/codeact_swe_agent.py

@@ -18,6 +18,7 @@ from opendevin.events.observation import (
     CmdOutputObservation,
     IPythonRunCellObservation,
 )
+from opendevin.events.serialization.event import truncate_content
 from opendevin.llm.llm import LLM
 from opendevin.runtime.plugins import (
     AgentSkillsRequirement,
@@ -52,7 +53,7 @@ def get_action_message(action: Action) -> dict[str, str] | None:
 
 def get_observation_message(obs) -> dict[str, str] | None:
     if isinstance(obs, CmdOutputObservation):
-        content = 'OBSERVATION:\n' + truncate_observation(obs.content)
+        content = 'OBSERVATION:\n' + truncate_content(obs.content)
         content += (
             f'\n[Command {obs.command_id} finished with exit code {obs.exit_code}]'
         )
@@ -67,25 +68,11 @@ def get_observation_message(obs) -> dict[str, str] | None:
                     '![image](data:image/png;base64, ...) already displayed to user'
                 )
         content = '\n'.join(splitted)
-        content = truncate_observation(content)
+        content = truncate_content(content)
         return {'role': 'user', 'content': content}
     return None
 
 
-def truncate_observation(observation: str, max_chars: int = 10_000) -> str:
-    """
-    Truncate the middle of the observation if it is too long.
-    """
-    if len(observation) <= max_chars:
-        return observation
-    half = max_chars // 2
-    return (
-        observation[:half]
-        + '\n[... Observation truncated due to length ...]\n'
-        + observation[-half:]
-    )
-
-
 def get_system_message() -> str:
     return f'{MINIMAL_SYSTEM_PREFIX}\n\n{COMMAND_DOCS}\n\n{SYSTEM_SUFFIX}'
 

+ 2 - 31
agenthub/monologue_agent/agent.py

@@ -159,14 +159,12 @@ class MonologueAgent(Agent):
             if not isinstance(prev_action, NullAction):
                 recent_events.append(event_to_memory(prev_action))
             if not isinstance(obs, NullObservation):
-                recent_events.append(self._truncate_output(event_to_memory(obs)))
+                recent_events.append(event_to_memory(obs))
 
         # add the last messages to long term memory
         if self.memory is not None and state.history and len(state.history) > 0:
             self.memory.add_event(event_to_memory(state.history[-1][0]))
-            self.memory.add_event(
-                self._truncate_output(event_to_memory(state.history[-1][1]))
-            )
+            self.memory.add_event(event_to_memory(state.history[-1][1]))
 
         # the action prompt with initial thoughts and recent events
         prompt = prompts.get_request_action_prompt(
@@ -187,33 +185,6 @@ class MonologueAgent(Agent):
         self.latest_action = action
         return action
 
-    def _truncate_output(
-        self, observation: dict, max_chars: int = MAX_OUTPUT_LENGTH
-    ) -> dict[str, str]:
-        """
-        Truncates the output of an observation to a maximum number of characters.
-
-        Parameters:
-        - output (str): The observation whose output to truncate
-        - max_chars (int): The maximum number of characters to allow
-
-        Returns:
-        - str: The truncated output
-        """
-        if (
-            'args' in observation
-            and 'output' in observation['args']
-            and len(observation['args']['output']) > max_chars
-        ):
-            output = observation['args']['output']
-            half = max_chars // 2
-            observation['args']['output'] = (
-                output[:half]
-                + '\n[... Output truncated due to length...]\n'
-                + output[-half:]
-            )
-        return observation
-
     def search_memory(self, query: str) -> list[str]:
         """
         Uses VectorIndexRetriever to find related memories within the long term memory.

+ 2 - 0
opendevin/core/config.py

@@ -39,6 +39,7 @@ class LLMConfig(metaclass=Singleton):
         retry_min_wait: The minimum time to wait between retries, in seconds. This is exponential backoff minimum. For models with very low limits, this can be set to 15-20.
         retry_max_wait: The maximum time to wait between retries, in seconds. This is exponential backoff maximum.
         timeout: The timeout for the API.
+        max_message_chars: The approximate max number of characters in the content of an event included in the prompt to the LLM. Larger observations are truncated.
         temperature: The temperature for the API.
         top_p: The top p for the API.
         custom_llm_provider: The custom LLM provider to use. This is undocumented in opendevin, and normally not used. It is documented on the litellm side.
@@ -62,6 +63,7 @@ class LLMConfig(metaclass=Singleton):
     retry_min_wait: int = 3
     retry_max_wait: int = 60
     timeout: int | None = None
+    max_message_chars: int = 10_000  # maximum number of characters in an observation's content when sent to the llm
     temperature: float = 0
     top_p: float = 0.5
     custom_llm_provider: str | None = None

+ 23 - 0
opendevin/events/serialization/event.py

@@ -1,7 +1,9 @@
 from dataclasses import asdict
 from datetime import datetime
 
+from opendevin.core.config import config
 from opendevin.events import Event, EventSource
+from opendevin.events.observation.observation import Observation
 
 from .action import action_from_dict
 from .observation import observation_from_dict
@@ -76,4 +78,25 @@ def event_to_memory(event: 'Event') -> dict:
     d.pop('message', None)
     if 'extras' in d:
         remove_fields(d['extras'], DELETE_FROM_MEMORY_EXTRAS)
+    if isinstance(event, Observation) and 'content' in d:
+        d['content'] = truncate_content(d['content'])
     return d
+
+
+def truncate_content(content: str, max_chars: int = -1) -> str:
+    """
+    Truncate the middle of the observation content if it is too long.
+    """
+    if max_chars == -1:
+        max_chars = config.llm.max_message_chars
+
+    if len(content) <= max_chars:
+        return content
+
+    # truncate the middle and include a message to the LLM about it
+    half = max_chars // 2
+    return (
+        content[:half]
+        + '\n[... Observation truncated due to length ...]\n'
+        + content[-half:]
+    )