feat: add tool decorator#387
feat: add tool decorator#387akihikokuroda wants to merge 6 commits intogenerative-computing:mainfrom
Conversation
Merge ProtectionsYour pull request matches the following merge protections and will not be merged until they are valid. 🟢 Enforce conventional commitWonderful, this rule succeeded.Make sure that we follow https://www.conventionalcommits.org/en/v1.0.0/
|
|
The PR description has been updated. Please fill out the template for your PR to be reviewed. |
| # Example: Define a custom tool using the @tool decorator | ||
| @tool | ||
| def get_weather(location: str, days: int = 1) -> dict: | ||
| """Get weather forecast for a location. | ||
|
|
||
| Args: | ||
| location: City name | ||
| days: Number of days to forecast (default: 1) | ||
| """ | ||
| # Mock implementation | ||
| return {"location": location, "days": days, "forecast": "sunny", "temperature": 72} | ||
|
|
||
|
|
||
| @tool(name="custom_calculator") | ||
| def calculate(expression: str) -> float: | ||
| """Evaluate a mathematical expression. | ||
|
|
||
| Args: | ||
| expression: Mathematical expression to evaluate | ||
| """ | ||
| # Simple mock - in production, use safe evaluation | ||
| return eval(expression) | ||
|
|
||
|
|
There was a problem hiding this comment.
Please remove these tool definitions. I don't think they are used in this file.
| # Before the decorator, you had to do: | ||
| # tools = [MelleaTool.from_callable(get_weather), MelleaTool.from_callable(search_web)] |
There was a problem hiding this comment.
Maybe change this comment to Without the decorator, you can add tools using:
mellea/backends/tools.py
Outdated
| def tool(func: Callable | None = None, *, name: str | None = None) -> Any: | ||
| """Decorator to mark a function as a Mellea tool. | ||
|
|
||
| This decorator wraps a function to make it usable as a tool without | ||
| requiring explicit MelleaTool.from_callable() calls. The decorated | ||
| function can be used both as a normal callable and as a MelleaTool. |
There was a problem hiding this comment.
I'm not certain about this implementation. I see two main issues:
- It diverges from our existing tool implementation. For existing tools, you use
.run()to call the underlying function with the appropriate arguments. We should unify this wrapper with that approach. It might require using generics with ParamSpec to get the typing correct forrun(). - The returned object isn't typed as a Tool by IDEs. I think this confusing because there is no real way to know if what you are passing in is a tool or a regular callable function.
mellea/backends/tools.py
Outdated
| # Extract MelleaTool from decorated functions | ||
| if hasattr(tool_instance, "_mellea_tool"): | ||
| tool_instance = tool_instance._mellea_tool |
There was a problem hiding this comment.
Shouldn't we be able to pass in the decorated function directly since it's typed as a MelleaTool and has all the same properties?
mellea/backends/tools.py
Outdated
| # Extract MelleaTool from decorated functions | ||
| if hasattr(tool_instance, "_mellea_tool"): | ||
| tool_instance = tool_instance._mellea_tool |
97edda8 to
cb4bad7
Compare
|
@jakelorocco Thanks for review. I think all comments are addressed. |
cb4bad7 to
5ef0aa8
Compare
| def tool( | ||
| func: Callable | None = None, *, name: str | None = None | ||
| ) -> MelleaTool | Callable[[Callable], MelleaTool]: |
There was a problem hiding this comment.
This doesn't work because of your typing. If you do something like:
@tool
def tooling():
...
tooling()
You get type hinting errors on tooling() since it thinks it's a regular MelleaTool. For now, I think we should just return a regular MelleaTool and force it to be called with <tool>.run() like the other tools created from from_callable. If users get annoyed, we can add this functionality here.
There was a problem hiding this comment.
I made changes. Thanks.
6f88ce1 to
5ebc020
Compare
jakelorocco
left a comment
There was a problem hiding this comment.
Sorry, I think I missed this in the last review or the overloading changed it. I think after this change we should be good. Thank you!
| @overload | ||
| def tool(func: Callable) -> MelleaTool: ... |
There was a problem hiding this comment.
Can we add , *, name: str | None = None to this function definition so that you can still change the tool name when you call it on a function directly:
def new_tool(): ...
differently_named_tool = tool(new_tool, name="different_name")
9b9503a to
17af033
Compare
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
17af033 to
4095165
Compare
Misc PR
Type of PR
Description
Testing