diff --git a/.gitignore b/.gitignore index a5c325a25..9fc2404ed 100644 --- a/.gitignore +++ b/.gitignore @@ -448,3 +448,7 @@ pyrightconfig.json .ionide # End of https://www.toptal.com/developers/gitignore/api/python,direnv,visualstudiocode,pycharm,macos,jetbrains + +# AI agent configs +.bob/ +.claude/ diff --git a/README.md b/README.md index 78c4cc447..e47cb1f56 100644 --- a/README.md +++ b/README.md @@ -64,9 +64,10 @@ pip install mellea > `mellea` comes with some additional packages as defined in our `pyproject.toml`. If you would like to install all the extra optional dependencies, please run the following commands: > > ```bash -> uv pip install "mellea[hf]" # for Huggingface extras and Alora capabilities. +> uv pip install "mellea[hf]" # for Huggingface extras and Alora capabilities > uv pip install "mellea[watsonx]" # for watsonx backend > uv pip install "mellea[docling]" # for docling +> uv pip install "mellea[smolagents]" # for HuggingFace smolagents tools > uv pip install "mellea[all]" # for all the optional dependencies > ``` > diff --git a/docs/examples/tools/README.md b/docs/examples/tools/README.md index 35fa9a310..d6f60f0a4 100644 --- a/docs/examples/tools/README.md +++ b/docs/examples/tools/README.md @@ -14,6 +14,14 @@ Comprehensive examples of using the code interpreter tool with LLMs. - Validating tool arguments - Local vs. sandboxed execution +### smolagents_example.py +Shows how to use pre-built tools from HuggingFace's smolagents library. + +**Key Features:** +- Loading existing smolagents tools (PythonInterpreterTool, WikipediaSearchTool, etc.) +- Converting to Mellea tools with `MelleaTool.from_smolagents()` +- Using tools from the HuggingFace ecosystem + ## Concepts Demonstrated - **Tool Definition**: Creating tools for LLM use diff --git a/docs/examples/tools/smolagents_example.py b/docs/examples/tools/smolagents_example.py new file mode 100644 index 000000000..23f9ab180 --- /dev/null +++ b/docs/examples/tools/smolagents_example.py @@ -0,0 +1,44 @@ +# pytest: ollama, llm +"""Example showing how to use pre-built HuggingFace smolagents tools with Mellea. + +This demonstrates loading existing tools from the smolagents ecosystem, +similar to how you can use langchain tools with MelleaTool.from_langchain(). + +The smolagents library provides various pre-built tools like: +- PythonInterpreterTool for code execution +- DuckDuckGoSearchTool for web search (requires ddgs package) +- WikipediaSearchTool for Wikipedia queries +- And many others from the HuggingFace ecosystem +""" + +from mellea import start_session +from mellea.backends import ModelOption +from mellea.backends.tools import MelleaTool + +try: + # Import a pre-built tool from smolagents + from smolagents import PythonInterpreterTool + + # Create the smolagents tool instance + python_tool_hf = PythonInterpreterTool() + + # Convert to Mellea tool - now you can use it with Mellea! + python_tool = MelleaTool.from_smolagents(python_tool_hf) + + # Use with Mellea session + m = start_session() + result = m.instruct( + "Calculate the sum of numbers from 1 to 10 using Python", + model_options={ModelOption.TOOLS: [python_tool]}, + tool_calls=True, + ) + + print(f"Response: {result}") + + if result.tool_calls: + calc_result = result.tool_calls[python_tool.name].call_func() + print(f"\nCalculation result: {calc_result}") + +except ImportError as e: + print("Please install smolagents: uv pip install 'mellea[smolagents]'") + print(f"Error: {e}") diff --git a/mellea/backends/tools.py b/mellea/backends/tools.py index ec24b96b7..6bdcc110e 100644 --- a/mellea/backends/tools.py +++ b/mellea/backends/tools.py @@ -75,8 +75,63 @@ def parameter_remapper(*args, **kwargs): except ImportError as e: raise ImportError( - f"It appears you are attempting to utilize a langchain tool '{type(tool)}'" - "Please install langchain core: pip install 'langchain_core'." + f"It appears you are attempting to utilize a langchain tool '{type(tool)}'. " + "Please install langchain core: uv pip install langchain-core" + ) from e + + @classmethod + def from_smolagents(cls, tool: Any): + """Create a Tool from a HuggingFace smolagents tool object. + + Args: + tool: A smolagents.Tool instance + + Returns: + MelleaTool: A Mellea tool wrapping the smolagents tool + + Raises: + ImportError: If smolagents is not installed + ValueError: If tool is not a smolagents Tool instance + + Example: + >>> from smolagents import PythonInterpreterTool + >>> tool = PythonInterpreterTool() + >>> mellea_tool = MelleaTool.from_smolagents(tool) + """ + try: + from smolagents import ( # type: ignore[import-not-found] + Tool as SmolagentsTool, + ) + from smolagents.models import ( # type: ignore[import-not-found] + get_tool_json_schema, + ) + + if not isinstance(tool, SmolagentsTool): + raise ValueError( + f"tool parameter must be a smolagents Tool type; got: {type(tool)}" + ) + + tool_name = tool.name + + # Use smolagents' built-in conversion to OpenAI format + as_json = get_tool_json_schema(tool) + + # Wrap the tool's forward method + def tool_call(*args, **kwargs): + """Wrapper for smolagents tool forward method.""" + if args: + # This shouldn't happen. Our ModelToolCall.call_func passes everything as kwargs. + FancyLogger.get_logger().warning( + f"ignoring unexpected args while calling smolagents tool ({tool_name}): ({args})" + ) + return tool.forward(**kwargs) + + return MelleaTool(tool_name, tool_call, as_json) + + except ImportError as e: + raise ImportError( + f"It appears you are attempting to utilize a smolagents tool '{type(tool)}'. " + "Please install mellea with smolagents support: uv pip install 'mellea[smolagents]'" ) from e @classmethod diff --git a/pyproject.toml b/pyproject.toml index 8f7223aa5..0568389bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,6 +101,10 @@ docling = [ "docling>=2.45.0", ] +smolagents = [ + "smolagents>=1.0.0", +] + telemetry = [ "opentelemetry-api>=1.20.0", "opentelemetry-sdk>=1.20.0", @@ -108,7 +112,7 @@ telemetry = [ "opentelemetry-distro>=0.59b0", ] -all = ["mellea[watsonx,docling,hf,vllm,litellm,telemetry]"] +all = ["mellea[watsonx,docling,hf,vllm,litellm,smolagents,telemetry]"] [dependency-groups] # Use these like: diff --git a/test/backends/test_mellea_tool.py b/test/backends/test_mellea_tool.py index 85b6a39db..8931f2982 100644 --- a/test/backends/test_mellea_tool.py +++ b/test/backends/test_mellea_tool.py @@ -119,5 +119,120 @@ def test_from_langchain_generation(session: MelleaSession): assert isinstance(tool.call_func(), str), "tool call did not yield expected type" +def test_from_smolagents_basic(): + """Test basic smolagents tool loading and schema conversion. + + This test verifies that: + 1. A smolagents tool can be wrapped as a MelleaTool + 2. The tool name is correctly extracted + 3. The schema is converted to OpenAI-compatible format + 4. The tool can be executed with arguments + """ + try: + from smolagents import Tool + except ImportError: + pytest.skip( + "smolagents not installed - install with: uv pip install 'mellea[smolagents]'" + ) + + # Create a simple smolagents tool + class SimpleTool(Tool): + name = "simple_tool" + description = "A simple test tool" + inputs = {"text": {"type": "string", "description": "Input text"}} + output_type = "string" + + def forward(self, text: str) -> str: + return f"Processed: {text}" + + hf_tool = SimpleTool() + mellea_tool = MelleaTool.from_smolagents(hf_tool) + + # Verify tool properties + assert isinstance(mellea_tool, MelleaTool) + assert mellea_tool.name == "simple_tool" + + # Verify schema conversion + json_schema = mellea_tool.as_json_tool + assert json_schema is not None + assert "function" in json_schema + assert json_schema["function"]["name"] == "simple_tool" + assert json_schema["function"]["description"] == "A simple test tool" + + # Verify parameters are present + assert "parameters" in json_schema["function"] + params = json_schema["function"]["parameters"] + assert "properties" in params + assert "text" in params["properties"] + + # Verify tool execution + result = mellea_tool.run(text="hello") + assert result == "Processed: hello" + + +def test_from_smolagents_multiple_inputs(): + """Test smolagents tool with multiple input parameters.""" + try: + from smolagents import Tool + except ImportError: + pytest.skip( + "smolagents not installed - install with: uv pip install 'mellea[smolagents]'" + ) + + class MultiInputTool(Tool): + name = "multi_input_tool" + description = "Tool with multiple inputs" + inputs = { + "x": {"type": "integer", "description": "First number"}, + "y": {"type": "integer", "description": "Second number"}, + "operation": {"type": "string", "description": "Operation to perform"}, + } + output_type = "integer" + + def forward(self, x: int, y: int, operation: str) -> int: + if operation == "add": + return x + y + elif operation == "multiply": + return x * y + return 0 + + hf_tool = MultiInputTool() + mellea_tool = MelleaTool.from_smolagents(hf_tool) + + # Verify all parameters are in schema + json_schema = mellea_tool.as_json_tool + params = json_schema["function"]["parameters"] + assert "x" in params["properties"] + assert "y" in params["properties"] + assert "operation" in params["properties"] + + # Verify tool execution with multiple args + result = mellea_tool.run(x=5, y=3, operation="add") + assert result == 8 + + result = mellea_tool.run(x=5, y=3, operation="multiply") + assert result == 15 + + +def test_from_smolagents_invalid_tool(): + """Test error handling for non-smolagents tool objects.""" + try: + from smolagents import Tool + except ImportError: + pytest.skip( + "smolagents not installed - install with: uv pip install 'mellea[smolagents]'" + ) + + # Try to create tool from non-Tool object + class NotATool: + name = "fake" + + with pytest.raises(ValueError) as exc_info: + MelleaTool.from_smolagents(NotATool()) + + error_msg = str(exc_info.value) + assert "smolagents Tool type" in error_msg + + if __name__ == "__main__": pytest.main([__file__]) diff --git a/test/telemetry/test_backend_telemetry.py b/test/telemetry/test_backend_telemetry.py index 428e0eb31..2b68fa87e 100644 --- a/test/telemetry/test_backend_telemetry.py +++ b/test/telemetry/test_backend_telemetry.py @@ -319,6 +319,3 @@ async def test_streaming_span_duration(span_exporter, gh_run): assert abs(span_duration_s - actual_duration) < 0.5, ( f"Streaming span duration {span_duration_s}s differs from actual {actual_duration}s" ) - - -# Made with Bob diff --git a/uv.lock b/uv.lock index f974d5bcb..5fedb597d 100644 --- a/uv.lock +++ b/uv.lock @@ -3445,6 +3445,7 @@ all = [ { name = "outlines" }, { name = "outlines-core" }, { name = "peft" }, + { name = "smolagents" }, { name = "transformers" }, { name = "trl" }, { name = "vllm" }, @@ -3467,6 +3468,9 @@ litellm = [ { name = "boto3" }, { name = "litellm" }, ] +smolagents = [ + { name = "smolagents" }, +] telemetry = [ { name = "opentelemetry-api" }, { name = "opentelemetry-distro" }, @@ -3533,7 +3537,7 @@ requires-dist = [ { name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.76" }, { name = "llm-sandbox", extras = ["docker"], specifier = ">=0.3.23" }, { name = "math-verify" }, - { name = "mellea", extras = ["watsonx", "docling", "hf", "vllm", "litellm", "telemetry"], marker = "extra == 'all'" }, + { name = "mellea", extras = ["watsonx", "docling", "hf", "vllm", "litellm", "smolagents", "telemetry"], marker = "extra == 'all'" }, { name = "mistletoe", specifier = ">=1.4.0" }, { name = "numpy", marker = "extra == 'vllm'", specifier = "<2.0.0" }, { name = "ollama", specifier = ">=0.5.1" }, @@ -3550,6 +3554,7 @@ requires-dist = [ { name = "pydantic" }, { name = "requests", specifier = ">=2.32.3" }, { name = "rouge-score" }, + { name = "smolagents", marker = "extra == 'smolagents'", specifier = ">=1.0.0" }, { name = "transformers", marker = "extra == 'hf'", specifier = ">=4.53.2,<5" }, { name = "transformers", marker = "extra == 'vllm'", specifier = "<4.54.0" }, { name = "trl", marker = "extra == 'hf'", specifier = "==0.19.1" }, @@ -3559,7 +3564,7 @@ requires-dist = [ { name = "uvicorn" }, { name = "vllm", marker = "extra == 'vllm'", specifier = ">=0.9.1" }, ] -provides-extras = ["hf", "vllm", "litellm", "watsonx", "docling", "telemetry", "all"] +provides-extras = ["hf", "vllm", "litellm", "watsonx", "docling", "smolagents", "telemetry", "all"] [package.metadata.requires-dev] dev = [ @@ -7336,6 +7341,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, ] +[[package]] +name = "smolagents" +version = "1.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "jinja2" }, + { name = "pillow" }, + { name = "python-dotenv" }, + { name = "requests" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/1c/8d8e7f39f3586dc85a7f96681b08b7c1cfbd734b258a025f368632f2d666/smolagents-1.24.0.tar.gz", hash = "sha256:4d5028ffbd72aca85fb2e8daac52d03fb7d4290a9bf99dbc311dd65980f6b5de", size = 225246, upload-time = "2026-01-16T05:37:04.156Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/7a/461ead649c088d30f7847ef1c70f245a9c188381ed09a264633831050497/smolagents-1.24.0-py3-none-any.whl", hash = "sha256:54853759d07a92a939f1ff59238b757fb1a153204c90b69324c8e9709025aa86", size = 155727, upload-time = "2026-01-16T05:37:02.903Z" }, +] + [[package]] name = "sniffio" version = "1.3.1"