CoolFace
Apppublic

Backup-bdg/OpenHands

sourceHugging Faceapache-2.0updated 1y agoView on Hugging Face
0likes
memory.py354 linesDownload Raw Back to memory
1import asyncio2import os3import uuid4from datetime import datetime, timezone5from typing import Callable6 7import openhands8from openhands.core.config.mcp_config import MCPConfig9from openhands.core.logger import openhands_logger as logger10from openhands.events.action.agent import RecallAction11from openhands.events.event import Event, EventSource, RecallType12from openhands.events.observation.agent import (13    MicroagentKnowledge,14    RecallObservation,15)16from openhands.events.observation.empty import NullObservation17from openhands.events.stream import EventStream, EventStreamSubscriber18from openhands.microagent import (19    BaseMicroagent,20    KnowledgeMicroagent,21    RepoMicroagent,22    load_microagents_from_dir,23)24from openhands.runtime.base import Runtime25from openhands.utils.prompt import (26    ConversationInstructions,27    RepositoryInfo,28    RuntimeInfo,29)30 31GLOBAL_MICROAGENTS_DIR = os.path.join(32    os.path.dirname(os.path.dirname(openhands.__file__)),33    'microagents',34)35 36 37class Memory:38    """39    Memory is a component that listens to the EventStream for information retrieval actions40    (a RecallAction) and publishes observations with the content (such as RecallObservation).41    """42 43    sid: str44    event_stream: EventStream45    status_callback: Callable | None46    loop: asyncio.AbstractEventLoop | None47 48    def __init__(49        self,50        event_stream: EventStream,51        sid: str,52        status_callback: Callable | None = None,53    ):54        self.event_stream = event_stream55        self.sid = sid if sid else str(uuid.uuid4())56        self.status_callback = status_callback57        self.loop = None58 59        self.event_stream.subscribe(60            EventStreamSubscriber.MEMORY,61            self.on_event,62            self.sid,63        )64 65        # Additional placeholders to store user workspace microagents66        self.repo_microagents: dict[str, RepoMicroagent] = {}67        self.knowledge_microagents: dict[str, KnowledgeMicroagent] = {}68 69        # Store repository / runtime info to send them to the templating later70        self.repository_info: RepositoryInfo | None = None71        self.runtime_info: RuntimeInfo | None = None72        self.conversation_instructions: ConversationInstructions | None = None73 74        # Load global microagents (Knowledge + Repo)75        # from typically OpenHands/microagents (i.e., the PUBLIC microagents)76        self._load_global_microagents()77 78    def on_event(self, event: Event):79        """Handle an event from the event stream."""80        asyncio.get_event_loop().run_until_complete(self._on_event(event))81 82    async def _on_event(self, event: Event):83        """Handle an event from the event stream asynchronously."""84        try:85            if isinstance(event, RecallAction):86                # if this is a workspace context recall (on first user message)87                # create and add a RecallObservation88                # with info about repo, runtime, instructions, etc. including microagent knowledge if any89                if (90                    event.source == EventSource.USER91                    and event.recall_type == RecallType.WORKSPACE_CONTEXT92                ):93                    logger.debug('Workspace context recall')94                    workspace_obs: RecallObservation | NullObservation | None = None95 96                    workspace_obs = self._on_workspace_context_recall(event)97                    if workspace_obs is None:98                        workspace_obs = NullObservation(content='')99 100                    # important: this will release the execution flow from waiting for the retrieval to complete101                    workspace_obs._cause = event.id  # type: ignore[union-attr]102 103                    self.event_stream.add_event(workspace_obs, EventSource.ENVIRONMENT)104                    return105 106                # Handle knowledge recall (triggered microagents)107                # Allow triggering from both user and agent messages108                elif (109                    event.source == EventSource.USER110                    or event.source == EventSource.AGENT111                ) and event.recall_type == RecallType.KNOWLEDGE:112                    logger.debug(113                        f'Microagent knowledge recall from {event.source} message'114                    )115                    microagent_obs: RecallObservation | NullObservation | None = None116                    microagent_obs = self._on_microagent_recall(event)117                    if microagent_obs is None:118                        microagent_obs = NullObservation(content='')119 120                    # important: this will release the execution flow from waiting for the retrieval to complete121                    microagent_obs._cause = event.id  # type: ignore[union-attr]122 123                    self.event_stream.add_event(microagent_obs, EventSource.ENVIRONMENT)124                    return125        except Exception as e:126            error_str = f'Error: {str(e.__class__.__name__)}'127            logger.error(error_str)128            self.send_error_message('STATUS$ERROR_MEMORY', error_str)129            return130 131    def _on_workspace_context_recall(132        self, event: RecallAction133    ) -> RecallObservation | None:134        """Add repository and runtime information to the stream as a RecallObservation.135 136        This method collects information from all available repo microagents and concatenates their contents.137        Multiple repo microagents are supported, and their contents will be concatenated with newlines between them.138        """139 140        # Create WORKSPACE_CONTEXT info:141        # - repository_info142        # - runtime_info143        # - repository_instructions144        # - microagent_knowledge145 146        # Collect raw repository instructions147        repo_instructions = ''148 149        # Retrieve the context of repo instructions from all repo microagents150        for microagent in self.repo_microagents.values():151            if repo_instructions:152                repo_instructions += '\n\n'153            repo_instructions += microagent.content154 155        # Find any matched microagents based on the query156        microagent_knowledge = self._find_microagent_knowledge(event.query)157 158        # Create observation if we have anything159        if (160            self.repository_info161            or self.runtime_info162            or repo_instructions163            or microagent_knowledge164            or self.conversation_instructions165        ):166            obs = RecallObservation(167                recall_type=RecallType.WORKSPACE_CONTEXT,168                repo_name=self.repository_info.repo_name169                if self.repository_info and self.repository_info.repo_name is not None170                else '',171                repo_directory=self.repository_info.repo_directory172                if self.repository_info173                and self.repository_info.repo_directory is not None174                else '',175                repo_instructions=repo_instructions if repo_instructions else '',176                runtime_hosts=self.runtime_info.available_hosts177                if self.runtime_info and self.runtime_info.available_hosts is not None178                else {},179                additional_agent_instructions=self.runtime_info.additional_agent_instructions180                if self.runtime_info181                and self.runtime_info.additional_agent_instructions is not None182                else '',183                microagent_knowledge=microagent_knowledge,184                content='Added workspace context',185                date=self.runtime_info.date if self.runtime_info is not None else '',186                custom_secrets_descriptions=self.runtime_info.custom_secrets_descriptions187                if self.runtime_info is not None188                else {},189                conversation_instructions=self.conversation_instructions.content190                if self.conversation_instructions is not None191                else '',192            )193            return obs194        return None195 196    def _on_microagent_recall(197        self,198        event: RecallAction,199    ) -> RecallObservation | None:200        """When a microagent action triggers microagents, create a RecallObservation with structured data."""201 202        # Find any matched microagents based on the query203        microagent_knowledge = self._find_microagent_knowledge(event.query)204 205        # Create observation if we have anything206        if microagent_knowledge:207            obs = RecallObservation(208                recall_type=RecallType.KNOWLEDGE,209                microagent_knowledge=microagent_knowledge,210                content='Retrieved knowledge from microagents',211            )212            return obs213        return None214 215    def _find_microagent_knowledge(self, query: str) -> list[MicroagentKnowledge]:216        """Find microagent knowledge based on a query.217 218        Args:219            query: The query to search for microagent triggers220 221        Returns:222            A list of MicroagentKnowledge objects for matched triggers223        """224        recalled_content: list[MicroagentKnowledge] = []225 226        # skip empty queries227        if not query:228            return recalled_content229 230        # Search for microagent triggers in the query231        for name, microagent in self.knowledge_microagents.items():232            trigger = microagent.match_trigger(query)233            if trigger:234                logger.info("Microagent '%s' triggered by keyword '%s'", name, trigger)235                recalled_content.append(236                    MicroagentKnowledge(237                        name=microagent.name,238                        trigger=trigger,239                        content=microagent.content,240                    )241                )242        return recalled_content243 244    def load_user_workspace_microagents(245        self, user_microagents: list[BaseMicroagent]246    ) -> None:247        """248        This method loads microagents from a user's cloned repo or workspace directory.249 250        This is typically called from agent_session or setup once the workspace is cloned.251        """252        logger.info(253            'Loading user workspace microagents: %s', [m.name for m in user_microagents]254        )255        for user_microagent in user_microagents:256            if isinstance(user_microagent, KnowledgeMicroagent):257                self.knowledge_microagents[user_microagent.name] = user_microagent258            elif isinstance(user_microagent, RepoMicroagent):259                self.repo_microagents[user_microagent.name] = user_microagent260 261    def _load_global_microagents(self) -> None:262        """263        Loads microagents from the global microagents_dir264        """265        repo_agents, knowledge_agents = load_microagents_from_dir(266            GLOBAL_MICROAGENTS_DIR267        )268        for name, agent in knowledge_agents.items():269            if isinstance(agent, KnowledgeMicroagent):270                self.knowledge_microagents[name] = agent271        for name, agent in repo_agents.items():272            if isinstance(agent, RepoMicroagent):273                self.repo_microagents[name] = agent274 275    def get_microagent_mcp_tools(self) -> list[MCPConfig]:276        """277        Get MCP tools from all repo microagents (always active)278 279        Returns:280            A list of MCP tools configurations from microagents281        """282        mcp_configs: list[MCPConfig] = []283 284        # Check all repo microagents for MCP tools (always active)285        for agent in self.repo_microagents.values():286            if agent.metadata.mcp_tools:287                mcp_configs.append(agent.metadata.mcp_tools)288                logger.debug(289                    f'Found MCP tools in repo microagent {agent.name}: {agent.metadata.mcp_tools}'290                )291 292        return mcp_configs293 294    def set_repository_info(self, repo_name: str, repo_directory: str) -> None:295        """Store repository info so we can reference it in an observation."""296        if repo_name or repo_directory:297            self.repository_info = RepositoryInfo(repo_name, repo_directory)298        else:299            self.repository_info = None300 301    def set_runtime_info(302        self,303        runtime: Runtime,304        custom_secrets_descriptions: dict[str, str],305    ) -> None:306        """Store runtime info (web hosts, ports, etc.)."""307        # e.g. { '127.0.0.1': 8080 }308        utc_now = datetime.now(timezone.utc)309        date = str(utc_now.date())310 311        if runtime.web_hosts or runtime.additional_agent_instructions:312            self.runtime_info = RuntimeInfo(313                available_hosts=runtime.web_hosts,314                additional_agent_instructions=runtime.additional_agent_instructions,315                date=date,316                custom_secrets_descriptions=custom_secrets_descriptions,317            )318        else:319            self.runtime_info = RuntimeInfo(320                date=date,321                custom_secrets_descriptions=custom_secrets_descriptions,322            )323 324    def set_conversation_instructions(325        self, conversation_instructions: str | None326    ) -> None:327        """328        Set contextual information for conversation329        This is information the agent may require330        """331        self.conversation_instructions = ConversationInstructions(332            content=conversation_instructions or ''333        )334 335    def send_error_message(self, message_id: str, message: str):336        """Sends an error message if the callback function was provided."""337        if self.status_callback:338            try:339                if self.loop is None:340                    self.loop = asyncio.get_running_loop()341                asyncio.run_coroutine_threadsafe(342                    self._send_status_message('error', message_id, message), self.loop343                )344            except RuntimeError as e:345                logger.error(346                    f'Error sending status message: {e.__class__.__name__}',347                    stack_info=False,348                )349 350    async def _send_status_message(self, msg_type: str, id: str, message: str):351        """Sends a status message to the client."""352        if self.status_callback:353            self.status_callback(msg_type, id, message)354