From 33d4312e1ad84f8b62cb085fe2216330bc3436e2 Mon Sep 17 00:00:00 2001 From: Aniket Maurya Date: Thu, 31 Jul 2025 13:30:27 +0000 Subject: [PATCH 1/9] Enhance LitTool: Add convert_tools method and update LLM class to utilize it --- src/litai/llm.py | 2 ++ src/litai/tools.py | 56 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/litai/llm.py b/src/litai/llm.py index eb75d38..85a7bca 100644 --- a/src/litai/llm.py +++ b/src/litai/llm.py @@ -298,6 +298,7 @@ def chat( # noqa: D417 str: The response from the LLM. """ self._wait_for_model() + tools = LitTool.convert_tools(tools) tool_schema = [tool.as_tool() for tool in tools] if tools else None if tool_schema: tool_context = ( @@ -359,6 +360,7 @@ def call_tool(response: str, tools: Optional[List[LitTool]] = None) -> Optional[ parsed = json.loads(response) tool_name = parsed["tool"] tool_args = parsed["parameters"] + tools = LitTool.convert_tools(tools) for tool in tools: if tool.name == tool_name: return tool.run(**tool_args) diff --git a/src/litai/tools.py b/src/litai/tools.py index 12f306d..8d78bbf 100644 --- a/src/litai/tools.py +++ b/src/litai/tools.py @@ -14,11 +14,16 @@ """Tools for the LLM.""" import json +from typing import List, Any, Union + from inspect import Signature, signature -from typing import Any, Callable, Dict, Optional, Union +from typing import Any, Callable, Dict, Optional, Union, TYPE_CHECKING from pydantic import BaseModel, ConfigDict, Field +if TYPE_CHECKING: + from langchain_core.tools.structured import StructuredTool + class LitTool(BaseModel): """A tool is a function that can be used to interact with the world.""" @@ -79,6 +84,55 @@ def as_tool(self, json_mode: bool = False) -> Union[str, Dict[str, Any]]: "parameters": self._extract_parameters(), } + @classmethod + def from_langchain(cls, tool: "StructuredTool"): + class LangchainTool(LitTool): + def setup(self) -> None: + super().setup() + self.name: str = tool.name + self.description: str = tool.description + self._tool = tool + + def run(self, *args: Any, **kwargs: Any) -> Any: + return self._tool.func(*args, **kwargs) + + def _extract_parameters(self): + return self._tool.args_schema.model_json_schema() + + return LangchainTool() + + @classmethod + def convert_tools(cls, tools: List[Any]) -> List["LitTool"]: + """Convert a list of tools into LitTool instances. + + - Passes through LitTool instances. + - Wraps LangChain StructuredTool objects. + - Raises TypeError for unsupported types. + """ + if tools is None: + return [] + if len(tools) == 0: + return [] + + lit_tools = [] + + for tool in tools: + if isinstance(tool, LitTool): + lit_tools.append(tool) + + # LangChain StructuredTool - check by type name and module + elif ( + type(tool).__name__ == "StructuredTool" and + type(tool).__module__ == "langchain_core.tools.structured" + ): + lit_tools.append(cls.from_langchain(tool)) + + else: + raise TypeError(f"Unsupported tool type: {type(tool)}") + + return lit_tools + + def tool(func: Optional[Callable] = None) -> Union[LitTool, Callable]: """Decorator to convert a function into a LitTool instance. From b205c887d32ffe10dc0b9b3851e5dd464166137e Mon Sep 17 00:00:00 2001 From: Aniket Maurya Date: Thu, 31 Jul 2025 13:55:48 +0000 Subject: [PATCH 2/9] Update requirements and add tests for LangChain tools integration --- _requirements/test.txt | 2 ++ tests/test_llm.py | 24 ++++++++++++++++++++++++ tests/test_tools.py | 13 +++++++++++++ 3 files changed, 39 insertions(+) diff --git a/_requirements/test.txt b/_requirements/test.txt index 82bfee5..c6cfc8b 100644 --- a/_requirements/test.txt +++ b/_requirements/test.txt @@ -5,3 +5,5 @@ mypy==1.16.1 psutil>=7.0.0 pytest-asyncio>=1.1.0 openai>=1.97.1 +langchain>=0.3.27 +langchain-core>=0.3.72 diff --git a/tests/test_llm.py b/tests/test_llm.py index 106d7c2..aeb9c48 100644 --- a/tests/test_llm.py +++ b/tests/test_llm.py @@ -7,6 +7,8 @@ import pytest +from langchain_core.tools import tool as langchain_tool + from litai import LLM, tool @@ -410,3 +412,25 @@ def test_dump_debug(mock_makedirs, mock_open): assert "Test response" in written_content assert "📛 Exception:" in written_content assert "Test exception" in written_content + + + +@patch("litai.llm.SDKLLM") +def test_call_langchain_tools(mock_sdkllm): + mock_sdkllm.return_value.chat.return_value = json.dumps( + { + "type": "function_call", + "tool": "get_weather", + "parameters": {"city": "London"} + } + ) + + @langchain_tool + def get_weather(city: str) -> str: + """Get the weather of a given city""" + return f"Weather in {city} is sunny." + + + llm = LLM() + result = llm.chat("how is the weather in London?", tools=[get_weather]) + assert llm.call_tool(result, tools=[get_weather]) == "Weather in London is sunny." diff --git a/tests/test_tools.py b/tests/test_tools.py index ed53497..5fe00e0 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -16,6 +16,7 @@ import pytest from litai import LitTool, tool +from langchain_core.tools import tool as langchain_tool @pytest.fixture @@ -182,3 +183,15 @@ def setup(self) -> None: assert tool_instance.state == 1, "State initialized with 1" tool_instance.state += 1 assert tool_instance.state == 2, "State not incremented. Should be 2" + + +def test_from_langchain(): + @langchain_tool + def get_weather(city: str) -> str: + """Get the weather of a given city""" + return f"Weather in {city} is sunny." + + lit_tool = LitTool.from_langchain(get_weather) + assert isinstance(lit_tool, LitTool) + assert lit_tool.name == "get_weather" + assert lit_tool.description == "Get the weather of a given city" From 268b1fb3c6872be4621abe9a40090f84ff70213e Mon Sep 17 00:00:00 2001 From: Aniket Maurya Date: Thu, 31 Jul 2025 13:57:03 +0000 Subject: [PATCH 3/9] add docstring --- src/litai/tools.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/litai/tools.py b/src/litai/tools.py index 8d78bbf..958d1f7 100644 --- a/src/litai/tools.py +++ b/src/litai/tools.py @@ -86,6 +86,7 @@ def as_tool(self, json_mode: bool = False) -> Union[str, Dict[str, Any]]: @classmethod def from_langchain(cls, tool: "StructuredTool"): + """Convert a LangChain StructuredTool to a LitTool.""" class LangchainTool(LitTool): def setup(self) -> None: super().setup() From a8e5a86d59001eb93bf5f0966cdfab38651705f9 Mon Sep 17 00:00:00 2001 From: Aniket Maurya Date: Thu, 31 Jul 2025 15:03:48 +0100 Subject: [PATCH 4/9] fix tests --- tests/test_llm.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/tests/test_llm.py b/tests/test_llm.py index aeb9c48..7f207a4 100644 --- a/tests/test_llm.py +++ b/tests/test_llm.py @@ -6,7 +6,6 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest - from langchain_core.tools import tool as langchain_tool from litai import LLM, tool @@ -414,23 +413,18 @@ def test_dump_debug(mock_makedirs, mock_open): assert "Test exception" in written_content - @patch("litai.llm.SDKLLM") def test_call_langchain_tools(mock_sdkllm): - mock_sdkllm.return_value.chat.return_value = json.dumps( - { - "type": "function_call", - "tool": "get_weather", - "parameters": {"city": "London"} - } - ) - @langchain_tool def get_weather(city: str) -> str: - """Get the weather of a given city""" + """Get the weather of a given city.""" return f"Weather in {city} is sunny." - llm = LLM() - result = llm.chat("how is the weather in London?", tools=[get_weather]) + with patch.object( + llm, + "chat", + return_value=json.dumps({"type": "function_call", "tool": "get_weather", "parameters": {"city": "London"}}), + ): + result = llm.chat("how is the weather in London?", tools=[get_weather]) assert llm.call_tool(result, tools=[get_weather]) == "Weather in London is sunny." From c41837fb296ca46dd273c6978b171c57a37565d1 Mon Sep 17 00:00:00 2001 From: Aniket Maurya Date: Thu, 31 Jul 2025 15:10:00 +0100 Subject: [PATCH 5/9] fix ci --- src/litai/tools.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/litai/tools.py b/src/litai/tools.py index 958d1f7..5ddda5f 100644 --- a/src/litai/tools.py +++ b/src/litai/tools.py @@ -14,10 +14,8 @@ """Tools for the LLM.""" import json -from typing import List, Any, Union - from inspect import Signature, signature -from typing import Any, Callable, Dict, Optional, Union, TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union from pydantic import BaseModel, ConfigDict, Field @@ -85,19 +83,20 @@ def as_tool(self, json_mode: bool = False) -> Union[str, Dict[str, Any]]: } @classmethod - def from_langchain(cls, tool: "StructuredTool"): + def from_langchain(cls, tool: "StructuredTool") -> "LitTool": """Convert a LangChain StructuredTool to a LitTool.""" + class LangchainTool(LitTool): def setup(self) -> None: super().setup() - self.name: str = tool.name + self.name: str = tool.name self.description: str = tool.description self._tool = tool def run(self, *args: Any, **kwargs: Any) -> Any: return self._tool.func(*args, **kwargs) - def _extract_parameters(self): + def _extract_parameters(self) -> Dict[str, Any]: return self._tool.args_schema.model_json_schema() return LangchainTool() @@ -122,10 +121,7 @@ def convert_tools(cls, tools: List[Any]) -> List["LitTool"]: lit_tools.append(tool) # LangChain StructuredTool - check by type name and module - elif ( - type(tool).__name__ == "StructuredTool" and - type(tool).__module__ == "langchain_core.tools.structured" - ): + elif type(tool).__name__ == "StructuredTool" and type(tool).__module__ == "langchain_core.tools.structured": lit_tools.append(cls.from_langchain(tool)) else: @@ -134,7 +130,6 @@ def convert_tools(cls, tools: List[Any]) -> List["LitTool"]: return lit_tools - def tool(func: Optional[Callable] = None) -> Union[LitTool, Callable]: """Decorator to convert a function into a LitTool instance. From 2b199d30f3ff8728ba4d61e695e9c56eb129fa7b Mon Sep 17 00:00:00 2001 From: Aniket Maurya Date: Thu, 31 Jul 2025 15:10:49 +0100 Subject: [PATCH 6/9] fix precommit --- tests/test_tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_tools.py b/tests/test_tools.py index 5fe00e0..5075dfc 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -14,9 +14,9 @@ """Unit tests for tools module.""" import pytest +from langchain_core.tools import tool as langchain_tool from litai import LitTool, tool -from langchain_core.tools import tool as langchain_tool @pytest.fixture @@ -188,7 +188,7 @@ def setup(self) -> None: def test_from_langchain(): @langchain_tool def get_weather(city: str) -> str: - """Get the weather of a given city""" + """Get the weather of a given city.""" return f"Weather in {city} is sunny." lit_tool = LitTool.from_langchain(get_weather) From 7eac5a61082003e0b92b2f112e1a34ada4995c05 Mon Sep 17 00:00:00 2001 From: Aniket Maurya Date: Thu, 31 Jul 2025 15:15:34 +0100 Subject: [PATCH 7/9] add tests --- tests/test_tools.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/test_tools.py b/tests/test_tools.py index 5075dfc..038127a 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -194,4 +194,23 @@ def get_weather(city: str) -> str: lit_tool = LitTool.from_langchain(get_weather) assert isinstance(lit_tool, LitTool) assert lit_tool.name == "get_weather" - assert lit_tool.description == "Get the weather of a given city" + assert lit_tool.description == "Get the weather of a given city." + assert lit_tool.as_tool() == { + "name": "get_weather", + "description": "Get the weather of a given city.", + "parameters": get_weather.args_schema.model_json_schema(), + } + + +def test_convert_tools_empty(): + lit_tools = LitTool.convert_tools([]) + assert len(lit_tools) == 0 + + +def test_convert_tools_unsupported_type(): + def get_weather(city: str) -> str: + """Get the weather of a given city.""" + return f"Weather in {city} is sunny." + + with pytest.raises(TypeError, match="Unsupported tool type: "): + LitTool.convert_tools([get_weather]) From de645cfeca0f50e1bbb369e318c5aa35ff6d9926 Mon Sep 17 00:00:00 2001 From: Aniket Maurya Date: Thu, 31 Jul 2025 14:41:53 +0000 Subject: [PATCH 8/9] fix mypy --- src/litai/tools.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/litai/tools.py b/src/litai/tools.py index 5ddda5f..d861a42 100644 --- a/src/litai/tools.py +++ b/src/litai/tools.py @@ -94,15 +94,15 @@ def setup(self) -> None: self._tool = tool def run(self, *args: Any, **kwargs: Any) -> Any: - return self._tool.func(*args, **kwargs) + return self._tool.func(*args, **kwargs) # type: ignore def _extract_parameters(self) -> Dict[str, Any]: - return self._tool.args_schema.model_json_schema() + return self._tool.args_schema.model_json_schema() # type: ignore return LangchainTool() @classmethod - def convert_tools(cls, tools: List[Any]) -> List["LitTool"]: + def convert_tools(cls, tools: Optional[List[Any]]) -> List["LitTool"]: """Convert a list of tools into LitTool instances. - Passes through LitTool instances. From 194d4ce247ed692bd788a9aab2338b74761f2fcc Mon Sep 17 00:00:00 2001 From: Aniket Maurya Date: Thu, 31 Jul 2025 14:43:10 +0000 Subject: [PATCH 9/9] ruff format --- src/litai/tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/litai/tools.py b/src/litai/tools.py index d861a42..c7e5159 100644 --- a/src/litai/tools.py +++ b/src/litai/tools.py @@ -94,10 +94,10 @@ def setup(self) -> None: self._tool = tool def run(self, *args: Any, **kwargs: Any) -> Any: - return self._tool.func(*args, **kwargs) # type: ignore + return self._tool.func(*args, **kwargs) # type: ignore def _extract_parameters(self) -> Dict[str, Any]: - return self._tool.args_schema.model_json_schema() # type: ignore + return self._tool.args_schema.model_json_schema() # type: ignore return LangchainTool()