feat(MCP, microagent): MCP-support for Repo Microagent & add fetch as default tool (#8360)

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
This commit is contained in:
Xingyao Wang
2025-05-17 07:32:38 +08:00
committed by GitHub
parent 819bad0777
commit 1f390430e5
20 changed files with 250 additions and 122 deletions

View File

@@ -0,0 +1,15 @@
---
# This is a repo microagent that is always activated
# to include necessary default tools implemented with MCP
name: default-tools
type: repo
version: 1.0.0
agent: CodeActAgent
mcp_tools:
stdio_servers:
- name: "fetch"
command: "uvx"
args: ["mcp-server-fetch"]
# We leave the body empty because MCP tools will automatically add the
# tool description for LLMs in tool calls, so there's no need to add extra descriptions.
---

View File

@@ -20,7 +20,6 @@ from openhands.agenthub.codeact_agent.tools.str_replace_editor import (
create_str_replace_editor_tool,
)
from openhands.agenthub.codeact_agent.tools.think import ThinkTool
from openhands.agenthub.codeact_agent.tools.web_read import WebReadTool
from openhands.controller.agent import Agent
from openhands.controller.state.state import State
from openhands.core.config import AgentConfig
@@ -123,7 +122,6 @@ class CodeActAgent(Agent):
if sys.platform == 'win32':
logger.warning('Windows runtime does not support browsing yet')
else:
tools.append(WebReadTool)
tools.append(BrowserTool)
if self.config.enable_jupyter:
tools.append(IPythonTool)

View File

@@ -15,7 +15,6 @@ from openhands.agenthub.codeact_agent.tools import (
IPythonTool,
LLMBasedFileEditTool,
ThinkTool,
WebReadTool,
create_cmd_run_tool,
create_str_replace_editor_tool,
)
@@ -212,16 +211,6 @@ def response_to_actions(
)
action = BrowseInteractiveAction(browser_actions=arguments['code'])
# ================================================
# WebReadTool (simplified browsing)
# ================================================
elif tool_call.function.name == WebReadTool['function']['name']:
if 'url' not in arguments:
raise FunctionCallValidationError(
f'Missing required argument "url" in tool call {tool_call.function.name}'
)
action = BrowseURLAction(url=arguments['url'])
# ================================================
# MCPAction (MCP)
# ================================================

View File

@@ -5,7 +5,6 @@ from .ipython import IPythonTool
from .llm_based_edit import LLMBasedFileEditTool
from .str_replace_editor import create_str_replace_editor_tool
from .think import ThinkTool
from .web_read import WebReadTool
__all__ = [
'BrowserTool',
@@ -14,6 +13,5 @@ __all__ = [
'IPythonTool',
'LLMBasedFileEditTool',
'create_str_replace_editor_tool',
'WebReadTool',
'ThinkTool',
]

View File

@@ -1,26 +0,0 @@
from litellm import ChatCompletionToolParam, ChatCompletionToolParamFunctionChunk
_WEB_DESCRIPTION = """Read (convert to markdown) content from a webpage. You should prefer using the `web_read` tool over the `browser` tool, but do use the `browser` tool if you need to interact with a webpage (e.g., click a button, fill out a form, etc.) OR read a webpage that contains images.
You may use the `web_read` tool to read text content from a webpage, and even search the webpage content using a Google search query (e.g., url=`https://www.google.com/search?q=YOUR_QUERY`).
Only the most recently read webpage will be available to read. This means you should not follow a link to a new page until you are done with the information on the current page.
"""
WebReadTool = ChatCompletionToolParam(
type='function',
function=ChatCompletionToolParamFunctionChunk(
name='web_read',
description=_WEB_DESCRIPTION,
parameters={
'type': 'object',
'properties': {
'url': {
'type': 'string',
'description': 'The URL of the webpage to read. You can also use a Google search query here (e.g., `https://www.google.com/search?q=YOUR_QUERY`).',
}
},
'required': ['url'],
},
),
)

View File

@@ -17,7 +17,6 @@ from openhands.agenthub.codeact_agent.function_calling import (
from openhands.agenthub.codeact_agent.tools import (
FinishTool,
ThinkTool,
WebReadTool,
)
from openhands.agenthub.readonly_agent.tools import (
GlobTool,
@@ -191,16 +190,6 @@ def response_to_actions(
glob_cmd = glob_to_cmdrun(pattern, path)
action = CmdRunAction(command=glob_cmd, is_input=False)
# ================================================
# WebReadTool (simplified browsing)
# ================================================
elif tool_call.function.name == WebReadTool['function']['name']:
if 'url' not in arguments:
raise FunctionCallValidationError(
f'Missing required argument "url" in tool call {tool_call.function.name}'
)
action = BrowseURLAction(url=arguments['url'])
# ================================================
# MCPAction (MCP)
# ================================================
@@ -249,7 +238,6 @@ def get_tools() -> list[ChatCompletionToolParam]:
return [
ThinkTool,
FinishTool,
WebReadTool,
GrepTool,
GlobTool,
ViewTool,

View File

@@ -232,7 +232,6 @@ async def run_session(
event_stream.subscribe(EventStreamSubscriber.MAIN, on_event, sid)
await runtime.connect()
await add_mcp_tools_to_agent(agent, runtime, config.mcp)
# Initialize repository if needed
repo_directory = None
@@ -251,6 +250,9 @@ async def run_session(
repo_directory=repo_directory,
)
# Add MCP tools to the agent
await add_mcp_tools_to_agent(agent, runtime, memory, config.mcp)
# Clear loading animation
is_loaded.set()

View File

@@ -116,8 +116,6 @@ async def run_controller(
selected_repository=config.sandbox.selected_repo,
)
await add_mcp_tools_to_agent(agent, runtime, config.mcp)
event_stream = runtime.event_stream
# when memory is created, it will load the microagents from the selected repository
@@ -130,6 +128,9 @@ async def run_controller(
repo_directory=repo_directory,
)
# Add MCP tools to the agent
await add_mcp_tools_to_agent(agent, runtime, memory, config.mcp)
replay_events: list[Event] | None = None
if config.replay_trajectory_path:
logger.info('Trajectory replay is enabled')

View File

@@ -10,6 +10,7 @@ from openhands.events.action.mcp import MCPAction
from openhands.events.observation.mcp import MCPObservation
from openhands.events.observation.observation import Observation
from openhands.mcp.client import MCPClient
from openhands.memory.memory import Memory
from openhands.runtime.base import Runtime
@@ -149,7 +150,7 @@ async def call_tool_mcp(mcp_clients: list[MCPClient], action: MCPAction) -> Obse
async def add_mcp_tools_to_agent(
agent: 'Agent', runtime: Runtime, mcp_config: MCPConfig
agent: 'Agent', runtime: Runtime, memory: 'Memory', mcp_config: MCPConfig
):
"""
Add MCP tools to an agent.
@@ -165,8 +166,25 @@ async def add_mcp_tools_to_agent(
'Runtime must be initialized before adding MCP tools'
)
# Add microagent MCP tools if available
microagent_mcp_configs = memory.get_microagent_mcp_tools()
extra_stdio_servers = []
for mcp_config in microagent_mcp_configs:
if mcp_config.sse_servers:
logger.warning(
'Microagent MCP config contains SSE servers, it is not yet supported.'
)
if mcp_config.stdio_servers:
for stdio_server in mcp_config.stdio_servers:
# Check if this stdio server is already in the config
if stdio_server not in extra_stdio_servers:
extra_stdio_servers.append(stdio_server)
logger.info(f'Added microagent stdio server: {stdio_server.name}')
# Add the runtime as another MCP server
updated_mcp_config = runtime.get_updated_mcp_config()
updated_mcp_config = runtime.get_updated_mcp_config(extra_stdio_servers)
# Fetch the MCP tools
mcp_tools = await fetch_mcp_tools_from_config(updated_mcp_config)

View File

@@ -5,6 +5,7 @@ from datetime import datetime, timezone
from typing import Callable
import openhands
from openhands.core.config.mcp_config import MCPConfig
from openhands.core.logger import openhands_logger as logger
from openhands.events.action.agent import RecallAction
from openhands.events.event import Event, EventSource, RecallType
@@ -262,6 +263,25 @@ class Memory:
if isinstance(agent, RepoMicroagent):
self.repo_microagents[name] = agent
def get_microagent_mcp_tools(self) -> list[MCPConfig]:
"""
Get MCP tools from all repo microagents (always active)
Returns:
A list of MCP tools configurations from microagents
"""
mcp_configs: list[MCPConfig] = []
# Check all repo microagents for MCP tools (always active)
for agent in self.repo_microagents.values():
if agent.metadata.mcp_tools:
mcp_configs.append(agent.metadata.mcp_tools)
logger.debug(
f'Found MCP tools in repo microagent {agent.name}: {agent.metadata.mcp_tools}'
)
return mcp_configs
def set_repository_info(self, repo_name: str, repo_directory: str) -> None:
"""Store repository info so we can reference it in an observation."""
if repo_name or repo_directory:

View File

@@ -64,6 +64,19 @@ class BaseMicroagent(BaseModel):
try:
metadata = MicroagentMetadata(**metadata_dict)
# Validate MCP tools configuration if present
if metadata.mcp_tools:
if metadata.mcp_tools.sse_servers:
logger.warning(
f'Microagent {metadata.name} has SSE servers. Only stdio servers are currently supported.'
)
if not metadata.mcp_tools.stdio_servers:
raise MicroagentValidationError(
f'Microagent {metadata.name} has MCP tools configuration but no stdio servers. '
'Only stdio servers are currently supported.'
)
except Exception as e:
# Provide more detailed error message for validation errors
error_msg = f'Error validating microagent metadata in {path.name}: {str(e)}'
@@ -81,13 +94,13 @@ class BaseMicroagent(BaseModel):
}
# Infer the agent type:
# 1. If triggers exist -> KNOWLEDGE
# 2. Else (no triggers) -> REPO
# 1. If triggers exist -> KNOWLEDGE (optional)
# 2. Else (no triggers) -> REPO (always active)
inferred_type: MicroagentType
if metadata.triggers:
inferred_type = MicroagentType.KNOWLEDGE
else:
# No triggers, default to REPO unless metadata explicitly says otherwise (which it shouldn't for REPO)
# No triggers, default to REPO
# This handles cases where 'type' might be missing or defaulted by Pydantic
inferred_type = MicroagentType.REPO_KNOWLEDGE
@@ -130,6 +143,7 @@ class KnowledgeMicroagent(BaseMicroagent):
for trigger in self.triggers:
if trigger.lower() in message:
return trigger
return None
@property
@@ -190,7 +204,9 @@ def load_microagents_from_dir(
repo_agents[agent.name] = agent
elif isinstance(agent, KnowledgeMicroagent):
knowledge_agents[agent.name] = agent
logger.debug(f'Loaded agent {agent.name} from {file}')
logger.debug(
f'Loaded agent {agent.name} from {file}. Type: {type(agent)}'
)
except MicroagentValidationError as e:
# For validation errors, include the original exception
error_msg = f'Error loading microagent from {file}: {str(e)}'

View File

@@ -2,12 +2,16 @@ from enum import Enum
from pydantic import BaseModel, Field
from openhands.core.config.mcp_config import (
MCPConfig,
)
class MicroagentType(str, Enum):
"""Type of microagent."""
KNOWLEDGE = 'knowledge'
REPO_KNOWLEDGE = 'repo'
KNOWLEDGE = 'knowledge' # Optional microagent, triggered by keywords
REPO_KNOWLEDGE = 'repo' # Always active microagent
class MicroagentMetadata(BaseModel):
@@ -18,3 +22,6 @@ class MicroagentMetadata(BaseModel):
version: str = Field(default='1.0.0')
agent: str = Field(default='CodeActAgent')
triggers: list[str] = [] # optional, only exists for knowledge microagents
mcp_tools: MCPConfig | None = (
None # optional, for microagents that provide additional MCP tools
)

View File

@@ -10,7 +10,7 @@ import httpx
from tenacity import retry, retry_if_exception, stop_after_attempt, wait_exponential
from openhands.core.config import AppConfig
from openhands.core.config.mcp_config import MCPConfig, MCPSSEServerConfig
from openhands.core.config.mcp_config import MCPConfig, MCPStdioServerConfig, MCPSSEServerConfig
from openhands.core.exceptions import (
AgentRuntimeTimeoutError,
)
@@ -351,7 +351,9 @@ class ActionExecutionClient(Runtime):
def browse_interactive(self, action: BrowseInteractiveAction) -> Observation:
return self.send_action_for_execution(action)
def get_updated_mcp_config(self) -> MCPConfig:
def get_updated_mcp_config(
self, extra_stdio_servers: list[MCPStdioServerConfig] | None = None
) -> MCPConfig:
# Add the runtime as another MCP server
updated_mcp_config = self.config.mcp.model_copy()
# Send a request to the action execution server to updated MCP config
@@ -359,6 +361,10 @@ class ActionExecutionClient(Runtime):
server.model_dump(mode='json')
for server in updated_mcp_config.stdio_servers
]
if extra_stdio_servers:
stdio_tools.extend(
[server.model_dump(mode='json') for server in extra_stdio_servers]
)
if len(stdio_tools) > 0:
self.log('debug', f'Updating MCP server to: {stdio_tools}')

View File

@@ -130,10 +130,27 @@ class AgentSession:
selected_branch=selected_branch,
)
repo_directory = None
if self.runtime and runtime_connected and selected_repository:
repo_directory = selected_repository.split('/')[-1]
if git_provider_tokens:
provider_handler = ProviderHandler(provider_tokens=git_provider_tokens)
await provider_handler.set_event_stream_secrets(self.event_stream)
if custom_secrets:
custom_secrets_handler.set_event_stream_secrets(self.event_stream)
self.memory = await self._create_memory(
selected_repository=selected_repository,
repo_directory=repo_directory,
custom_secrets_descriptions=custom_secrets_handler.get_custom_secrets_descriptions()
)
# NOTE: this needs to happen before controller is created
# so MCP tools can be included into the SystemMessageAction
if self.runtime and runtime_connected:
await add_mcp_tools_to_agent(agent, self.runtime, config.mcp)
await add_mcp_tools_to_agent(agent, self.runtime, self.memory, config.mcp)
if replay_json:
initial_message = self._run_replay(
@@ -156,23 +173,6 @@ class AgentSession:
agent_configs=agent_configs,
)
repo_directory = None
if self.runtime and runtime_connected and selected_repository:
repo_directory = selected_repository.split('/')[-1]
self.memory = await self._create_memory(
selected_repository=selected_repository,
repo_directory=repo_directory,
custom_secrets_descriptions=custom_secrets_handler.get_custom_secrets_descriptions()
)
if git_provider_tokens:
provider_handler = ProviderHandler(provider_tokens=git_provider_tokens)
await provider_handler.set_event_stream_secrets(self.event_stream)
if custom_secrets:
custom_secrets_handler.set_event_stream_secrets(self.event_stream)
if not self._closed:
if initial_message:
self.event_stream.add_event(initial_message, EventSource.USER)

View File

@@ -1,12 +1,18 @@
"""Tests for microagent loading in runtime."""
import os
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from conftest import (
_close_test_runtime,
_load_runtime,
)
from openhands.core.config import MCPConfig
from openhands.core.config.mcp_config import MCPStdioServerConfig
from openhands.mcp.utils import add_mcp_tools_to_agent
from openhands.microagent import KnowledgeMicroagent, RepoMicroagent
@@ -165,3 +171,91 @@ Repository-specific test instructions.
finally:
_close_test_runtime(runtime)
def test_default_tools_microagent_exists():
"""Test that the default-tools microagent exists in the global microagents directory."""
# Get the path to the global microagents directory
import openhands
project_root = os.path.dirname(openhands.__file__)
parent_dir = os.path.dirname(project_root)
microagents_dir = os.path.join(parent_dir, 'microagents')
# Check that the default-tools.md file exists
default_tools_path = os.path.join(microagents_dir, 'default-tools.md')
assert os.path.exists(default_tools_path), (
f'default-tools.md not found at {default_tools_path}'
)
# Read the file and check its content
with open(default_tools_path, 'r') as f:
content = f.read()
# Verify it's a repo microagent (always activated)
assert 'type: repo' in content, 'default-tools.md should be a repo microagent'
# Verify it has the fetch tool configured
assert 'name: "fetch"' in content, 'default-tools.md should have a fetch tool'
assert 'command: "uvx"' in content, 'default-tools.md should use uvx command'
assert 'args: ["mcp-server-fetch"]' in content, (
'default-tools.md should use mcp-server-fetch'
)
@pytest.mark.asyncio
async def test_add_mcp_tools_from_microagents():
"""Test that add_mcp_tools_to_agent adds tools from microagents."""
# Import ActionExecutionClient for mocking
from openhands.runtime.impl.action_execution.action_execution_client import (
ActionExecutionClient,
)
# Create mock objects
mock_agent = MagicMock()
mock_runtime = MagicMock(spec=ActionExecutionClient)
mock_memory = MagicMock()
mock_mcp_config = MCPConfig()
# Configure the mock memory to return a microagent MCP config
mock_stdio_server = MCPStdioServerConfig(
name='test-tool', command='test-command', args=['test-arg1', 'test-arg2']
)
mock_microagent_mcp_config = MCPConfig(stdio_servers=[mock_stdio_server])
mock_memory.get_microagent_mcp_tools.return_value = [mock_microagent_mcp_config]
# Configure the mock runtime
mock_runtime.runtime_initialized = True
mock_runtime.get_updated_mcp_config.return_value = mock_microagent_mcp_config
# Mock the fetch_mcp_tools_from_config function to return a mock tool
mock_tool = {
'type': 'function',
'function': {
'name': 'test-tool',
'description': 'Test tool description',
'parameters': {},
},
}
with patch(
'openhands.mcp.utils.fetch_mcp_tools_from_config',
new=AsyncMock(return_value=[mock_tool]),
):
# Call the function
await add_mcp_tools_to_agent(
mock_agent, mock_runtime, mock_memory, mock_mcp_config
)
# Verify that the memory's get_microagent_mcp_tools was called
mock_memory.get_microagent_mcp_tools.assert_called_once()
# Verify that the runtime's get_updated_mcp_config was called with the extra stdio servers
mock_runtime.get_updated_mcp_config.assert_called_once()
args, kwargs = mock_runtime.get_updated_mcp_config.call_args
assert len(args) == 1
assert len(args[0]) == 1
assert args[0][0].name == 'test-tool'
# Verify that the agent's set_mcp_tools was called with the mock tool
mock_agent.set_mcp_tools.assert_called_once_with([mock_tool])

View File

@@ -103,6 +103,8 @@ def mock_memory() -> Memory:
spec=Memory,
event_stream=test_event_stream,
)
# Add the get_microagent_mcp_tools method to the mock
memory.get_microagent_mcp_tools.return_value = []
return memory
@@ -740,7 +742,7 @@ async def test_notify_on_llm_retry(mock_agent, mock_event_stream, mock_status_ca
@pytest.mark.asyncio
async def test_context_window_exceeded_error_handling(
mock_agent, mock_runtime, test_event_stream
mock_agent, mock_runtime, test_event_stream, mock_memory
):
"""Test that context window exceeded errors are handled correctly by the controller, providing a smaller view but keeping the history intact."""
max_iterations = 5

View File

@@ -13,7 +13,6 @@ from openhands.agenthub.codeact_agent.tools import (
IPythonTool,
LLMBasedFileEditTool,
ThinkTool,
WebReadTool,
create_cmd_run_tool,
create_str_replace_editor_tool,
)
@@ -79,7 +78,6 @@ def test_agent_with_default_config_has_default_tools():
'finish',
'str_replace_editor',
'think',
'web_read',
}.issubset(default_tool_names)
@@ -179,13 +177,6 @@ def test_str_replace_editor_tool():
]
def test_web_read_tool():
assert WebReadTool['type'] == 'function'
assert WebReadTool['function']['name'] == 'web_read'
assert 'url' in WebReadTool['function']['parameters']['properties']
assert WebReadTool['function']['parameters']['required'] == ['url']
def test_browser_tool():
assert BrowserTool['type'] == 'function'
assert BrowserTool['function']['name'] == 'browser'

View File

@@ -132,7 +132,9 @@ def mock_settings_store():
@patch('openhands.cli.main.add_mcp_tools_to_agent')
@patch('openhands.cli.main.create_runtime')
@patch('openhands.cli.main.create_controller')
@patch('openhands.cli.main.create_memory')
@patch(
'openhands.cli.main.create_memory',
)
@patch('openhands.cli.main.run_agent_until_done')
@patch('openhands.cli.main.cleanup_session')
@patch('openhands.cli.main.initialize_repository_for_runtime')
@@ -168,7 +170,8 @@ async def test_run_session_without_initial_action(
mock_controller_task = MagicMock()
mock_create_controller.return_value = (mock_controller, mock_controller_task)
mock_memory = AsyncMock()
# Create a regular MagicMock for memory to avoid coroutine issues
mock_memory = MagicMock()
mock_create_memory.return_value = mock_memory
with patch(
@@ -197,7 +200,7 @@ async def test_run_session_without_initial_action(
mock_display_animation.assert_called_once()
mock_create_agent.assert_called_once_with(mock_config)
mock_add_mcp_tools.assert_called_once_with(
mock_agent, mock_runtime, mock_config.mcp
mock_agent, mock_runtime, mock_memory, mock_config.mcp
)
mock_create_runtime.assert_called_once()
mock_create_controller.assert_called_once()
@@ -220,7 +223,7 @@ async def test_run_session_without_initial_action(
@patch('openhands.cli.main.add_mcp_tools_to_agent')
@patch('openhands.cli.main.create_runtime')
@patch('openhands.cli.main.create_controller')
@patch('openhands.cli.main.create_memory')
@patch('openhands.cli.main.create_memory', new_callable=AsyncMock)
@patch('openhands.cli.main.run_agent_until_done')
@patch('openhands.cli.main.cleanup_session')
@patch('openhands.cli.main.initialize_repository_for_runtime')

View File

@@ -10,7 +10,6 @@ from openhands.agenthub.codeact_agent.function_calling import response_to_action
from openhands.core.exceptions import FunctionCallValidationError
from openhands.events.action import (
BrowseInteractiveAction,
BrowseURLAction,
CmdRunAction,
FileEditAction,
FileReadAction,
@@ -189,23 +188,6 @@ def test_browser_missing_code():
assert 'Missing required argument "code"' in str(exc_info.value)
def test_web_read_valid():
"""Test web_read with valid arguments."""
response = create_mock_response('web_read', {'url': 'https://example.com'})
actions = response_to_actions(response)
assert len(actions) == 1
assert isinstance(actions[0], BrowseURLAction)
assert actions[0].url == 'https://example.com'
def test_web_read_missing_url():
"""Test web_read with missing url argument."""
response = create_mock_response('web_read', {})
with pytest.raises(FunctionCallValidationError) as exc_info:
response_to_actions(response)
assert 'Missing required argument "url"' in str(exc_info.value)
def test_invalid_json_arguments():
"""Test handling of invalid JSON in arguments."""
response = ModelResponse(

View File

@@ -191,11 +191,23 @@ async def test_memory_with_microagents():
assert isinstance(observation, RecallObservation)
assert source == EventSource.ENVIRONMENT
assert observation.recall_type == RecallType.KNOWLEDGE
# We should have at least one microagent: flarglebargle (triggered by keyword)
# Note: The default-tools microagent might not be loaded in tests
assert len(observation.microagent_knowledge) == 1
# Find the flarglebargle microagent in the list
flarglebargle_knowledge = None
for knowledge in observation.microagent_knowledge:
if knowledge.name == derived_name:
flarglebargle_knowledge = knowledge
break
# Check against the derived name
assert observation.microagent_knowledge[0].name == derived_name
assert observation.microagent_knowledge[0].trigger == 'flarglebargle'
assert 'magic word' in observation.microagent_knowledge[0].content
assert flarglebargle_knowledge is not None
assert flarglebargle_knowledge.name == derived_name
assert flarglebargle_knowledge.trigger == 'flarglebargle'
assert 'magic word' in flarglebargle_knowledge.content
def test_memory_repository_info(prompt_dir, file_store):
@@ -321,11 +333,23 @@ async def test_memory_with_agent_microagents():
assert isinstance(observation, RecallObservation)
assert source == EventSource.ENVIRONMENT
assert observation.recall_type == RecallType.KNOWLEDGE
# We should have at least one microagent: flarglebargle (triggered by keyword)
# Note: The default-tools microagent might not be loaded in tests
assert len(observation.microagent_knowledge) == 1
# Find the flarglebargle microagent in the list
flarglebargle_knowledge = None
for knowledge in observation.microagent_knowledge:
if knowledge.name == derived_name:
flarglebargle_knowledge = knowledge
break
# Check against the derived name
assert observation.microagent_knowledge[0].name == derived_name
assert observation.microagent_knowledge[0].trigger == 'flarglebargle'
assert 'magic word' in observation.microagent_knowledge[0].content
assert flarglebargle_knowledge is not None
assert flarglebargle_knowledge.name == derived_name
assert flarglebargle_knowledge.trigger == 'flarglebargle'
assert 'magic word' in flarglebargle_knowledge.content
@pytest.mark.asyncio