Skip to content

Commit 77023f5

Browse files
authored
refactor FinishTool to builtin tools for agents (#39)
1 parent 855160a commit 77023f5

File tree

7 files changed

+102
-43
lines changed

7 files changed

+102
-43
lines changed

.openhands/microagents/repo.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ This repo has two python packages, with unit tests specifically written for each
163163

164164
<CODE>
165165
- Avoid hacky trick like `sys.path.insert` when resolving package dependency
166+
- Use existing packages/libraries instead of implementing yourselves whenever possible.
166167
</CODE>
167168

168169
<TESTING>

openhands/core/agent/codeact_agent/codeact_agent.py

Lines changed: 9 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -8,52 +8,19 @@
88
Message as LiteLLMMessage,
99
ModelResponse,
1010
)
11-
from pydantic import Field, ValidationError
11+
from pydantic import ValidationError
1212

1313
from openhands.core.context import EnvContext, PromptManager
1414
from openhands.core.conversation import ConversationCallbackType, ConversationState
1515
from openhands.core.llm import LLM, Message, TextContent, get_llm_metadata
1616
from openhands.core.logger import get_logger
17-
from openhands.core.tool import ActionBase, ObservationBase, Tool, ToolAnnotations
17+
from openhands.core.tool import BUILT_IN_TOOLS, ActionBase, FinishTool, ObservationBase, Tool
1818

1919
from ..base import AgentBase
2020

2121

2222
logger = get_logger(__name__)
2323

24-
"""Finish tool implementation."""
25-
26-
27-
class FinishAction(ActionBase):
28-
message: str = Field(description="Final message to send to the user.")
29-
30-
31-
TOOL_DESCRIPTION = """Signals the completion of the current task or conversation.
32-
33-
Use this tool when:
34-
- You have successfully completed the user's requested task
35-
- You cannot proceed further due to technical limitations or missing information
36-
37-
The message should include:
38-
- A clear summary of actions taken and their results
39-
- Any next steps for the user
40-
- Explanation if you're unable to complete the task
41-
- Any follow-up questions if more information is needed
42-
"""
43-
44-
45-
FINISH_TOOL = Tool(
46-
name="finish",
47-
input_schema=FinishAction,
48-
description=TOOL_DESCRIPTION,
49-
annotations=ToolAnnotations(
50-
title="finish",
51-
readOnlyHint=True,
52-
destructiveHint=False,
53-
idempotentHint=True,
54-
openWorldHint=False,
55-
),
56-
)
5724

5825

5926
class CodeActAgent(AgentBase):
@@ -65,8 +32,9 @@ def __init__(
6532
system_prompt_filename: str = "system_prompt.j2",
6633
cli_mode: bool = True,
6734
) -> None:
68-
assert FINISH_TOOL not in tools, "Finish tool is automatically included and should not be provided."
69-
super().__init__(llm=llm, tools=tools + [FINISH_TOOL], env_context=env_context)
35+
for tool in BUILT_IN_TOOLS:
36+
assert tool not in tools, f"{tool} is automatically included and should not be provided."
37+
super().__init__(llm=llm, tools=tools + BUILT_IN_TOOLS, env_context=env_context)
7038
self.prompt_manager = PromptManager(
7139
prompt_dir=os.path.join(os.path.dirname(__file__), "prompts"),
7240
system_prompt_filename=system_prompt_filename,
@@ -169,12 +137,6 @@ def _handle_tool_call(
169137
state.history.messages.append(Message(role="tool", name=tool.name, tool_call_id=tool_call.id, content=[TextContent(text=err)]))
170138
return state
171139

172-
# Early return for finish action (no need for tool execution)
173-
if isinstance(action, FinishAction):
174-
assert tool.name == FINISH_TOOL.name, "FinishAction must be used with the finish tool"
175-
state.agent_finished = True
176-
return state
177-
178140
# Execute actions!
179141
if tool.executor is None:
180142
raise RuntimeError(f"Tool '{tool.name}' has no executor")
@@ -188,4 +150,8 @@ def _handle_tool_call(
188150
state.history.messages.append(tool_msg)
189151
if on_event:
190152
on_event(observation)
153+
154+
# Set conversation state
155+
if tool.name == FinishTool.name:
156+
state.agent_finished = True
191157
return state

openhands/core/context/manager.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
2+
class LLMContextManager:
3+
"""Context manager for messages we send to LLM.
4+
5+
6+
"""
7+
def __init__(self) -> None:
8+
pass
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from openhands.core.tool.builtins import BUILT_IN_TOOLS
2+
3+
4+
def test_all_tools_property():
5+
for tool in BUILT_IN_TOOLS:
6+
assert tool.description is not None
7+
assert tool.input_schema is not None
8+
assert tool.output_schema is not None
9+
assert tool.executor is not None
10+
assert tool.annotations is not None
11+
# Annotations should have specific hints
12+
# Builtin tools should have all these properties
13+
assert tool.annotations.readOnlyHint
14+
assert not tool.annotations.destructiveHint
15+
assert tool.annotations.idempotentHint
16+
assert not tool.annotations.openWorldHint

openhands/core/tool/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""OpenHands runtime package."""
22

3+
from .builtins import BUILT_IN_TOOLS, FinishTool
34
from .tool import ActionBase, ObservationBase, Tool, ToolAnnotations, ToolExecutor
45

56

@@ -9,4 +10,6 @@
910
"ToolExecutor",
1011
"ActionBase",
1112
"ObservationBase",
13+
"FinishTool",
14+
"BUILT_IN_TOOLS",
1215
]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""Implementing essential tools that doesn't interact with the environment.
2+
3+
These are built in and are *required* for the agent to work.
4+
5+
For tools that require interacting with the environment, add them to `openhands/tools`.
6+
"""
7+
8+
from .finish import FinishAction, FinishObservation, FinishTool
9+
10+
11+
BUILT_IN_TOOLS = [FinishTool]
12+
13+
__all__ = [
14+
"BUILT_IN_TOOLS",
15+
"FinishTool",
16+
"FinishAction",
17+
"FinishObservation",
18+
]
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from pydantic import Field
2+
3+
from ..tool import ActionBase, ObservationBase, Tool, ToolAnnotations, ToolExecutor
4+
5+
6+
class FinishAction(ActionBase):
7+
message: str = Field(description="Final message to send to the user.")
8+
9+
class FinishObservation(ObservationBase):
10+
message: str = Field(description="Final message sent to the user.")
11+
12+
@property
13+
def agent_observation(self) -> str:
14+
return self.message
15+
16+
TOOL_DESCRIPTION = """Signals the completion of the current task or conversation.
17+
18+
Use this tool when:
19+
- You have successfully completed the user's requested task
20+
- You cannot proceed further due to technical limitations or missing information
21+
22+
The message should include:
23+
- A clear summary of actions taken and their results
24+
- Any next steps for the user
25+
- Explanation if you're unable to complete the task
26+
- Any follow-up questions if more information is needed
27+
"""
28+
29+
class FinishExecutor(ToolExecutor):
30+
def __call__(self, action: FinishAction) -> FinishObservation:
31+
return FinishObservation(message=action.message)
32+
33+
34+
FinishTool = Tool(
35+
name="finish",
36+
input_schema=FinishAction,
37+
output_schema=FinishObservation,
38+
description=TOOL_DESCRIPTION,
39+
executor=FinishExecutor(),
40+
annotations=ToolAnnotations(
41+
title="finish",
42+
readOnlyHint=True,
43+
destructiveHint=False,
44+
idempotentHint=True,
45+
openWorldHint=False,
46+
),
47+
)

0 commit comments

Comments
 (0)