Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/external libload #43

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/app/app/schemas/tool_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class ToolConfig(BaseModel):
system_context_refinement: Optional[str]
prompt_inputs: list[PromptInput]
additional: Optional[Box] = None
class_name: Optional[str] = None


class SqlToolConfig(ToolConfig):
Expand Down
120 changes: 61 additions & 59 deletions backend/app/app/services/chat_agent/tools/tools.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,32 @@
# -*- coding: utf-8 -*-
# pylint: disable=cyclic-import
from typing import List, Tuple, Type
import copy
import importlib
import logging
from typing import List

from langchain.tools import BaseTool

from app.services.chat_agent.tools.ExtendedBaseTool import ExtendedBaseTool
from app.services.chat_agent.tools.library.basellm_tool.basellm_tool import BaseLLM
from app.services.chat_agent.tools.library.image_generation_tool.image_generation_tool import ImageGenerationTool
from app.services.chat_agent.tools.library.pdf_tool.pdf_tool import PDFTool
from app.services.chat_agent.tools.library.sql_tool.sql_tool import SQLTool
from app.services.chat_agent.tools.library.summarizer_tool.summarizer_tool import SummarizerTool
from app.services.chat_agent.tools.library.visualizer_tool.visualizer_tool import JsxVisualizerTool
from app.utils.config_loader import get_agent_config

logger = logging.getLogger(__name__)

def get_nested_classes() -> List[Tuple[str, Type[ExtendedBaseTool]]]:
"""separated to avoid circular imports."""
from app.services.chat_agent.tools.library.chain_tool.nested_meta_agent_tool import ( # pylint: disable=import-outside-toplevel # noqa:E501
ChainTool,
)

nested_classes = [
("chain_tool", ChainTool),
]
return nested_classes # type: ignore
STATIC_SET_OF_TOOLS = {
"sql_tool": ["app.services.chat_agent.tools.library.sql_tool.sql_tool", "SQLTool"],
"visualizer_tool": ["app.services.chat_agent.tools.library.visualizer_tool.visualizer_tool", "JsxVisualizerTool"],
"summarizer_tool": ["app.services.chat_agent.tools.library.summarizer_tool.summarizer_tool", "SummarizerTool"],
"pdf_tool": ["app.services.chat_agent.tools.library.pdf_tool.pdf_tool", "PDFTool"],
"image_generation_tool": [
"app.services.chat_agent.tools.library.image_generation_tool.image_generation_tool",
"ImageGenerationTool",
],
"clarify_tool": ["app.services.chat_agent.tools.library.basellm_tool.basellm_tool", "BaseLLM"],
"expert_tool": ["app.services.chat_agent.tools.library.basellm_tool.basellm_tool", "BaseLLM"],
"entertainer_tool": ["app.services.chat_agent.tools.library.basellm_tool.basellm_tool", "BaseLLM"],
}


def get_tools(tools: List[str], load_nested: bool = True) -> List[BaseTool]:
Expand All @@ -46,48 +49,47 @@ def get_tools(tools: List[str], load_nested: bool = True) -> List[BaseTool]:
ValueError: If any tool name in the input list is not in the list of all tools.
"""
agent_config = get_agent_config()
all_tool_classes = [
(
"sql_tool",
SQLTool,
),
(
"visualizer_tool",
JsxVisualizerTool,
),
(
"summarizer_tool",
SummarizerTool,
),
(
"pdf_tool",
PDFTool,
),
(
"image_generation_tool",
ImageGenerationTool,
),
("clarify_tool", BaseLLM),
("expert_tool", BaseLLM),
("entertainer_tool", BaseLLM),
]
static_set_of_tools = copy.deepcopy(STATIC_SET_OF_TOOLS)

if load_nested:
all_tool_classes.extend(get_nested_classes())
all_tools: list[ExtendedBaseTool] = [
c.from_config( # type: ignore
config=agent_config.tools_library.library[name],
common_config=agent_config.common,
**({"name": name} if issubclass(c, BaseLLM) else {}),
)
for (
name,
c,
) in all_tool_classes
if name in agent_config.tools
]
tools_map = {tool.name: tool for tool in all_tools}

if any(tool_name not in tools_map for tool_name in tools):
raise ValueError(f"Invalid tool name(s): {[tool_name for tool_name in tools if tool_name not in tools_map]}")

return [tools_map[tool_name] for tool_name in tools]
static_set_of_tools["chain_tool"] = [
"app.services.chat_agent.tools.library.chain_tool.nested_meta_agent_tool",
"ChainTool",
]

# first consolidate all tools classes.
all_tool_classes = {}
for tool_name in tools:
# if the tool has a class_name definition I pick it, client comes first :)
if configured_classname := agent_config.tools_library.library[tool_name].class_name:
module_name, class_name = configured_classname.split(":")
logger.debug("Loading CustomTool %s from module %s with class %s", tool_name, module_name, class_name)

else:
# otherwise I pick it from the static set of tools.
module_name, class_name = static_set_of_tools[tool_name]
logger.debug(
"Loading AgentKit tool class %s from module %s with class %s", tool_name, module_name, class_name
)

# import the module and hook the class

module = importlib.import_module(f"{module_name}")
tool_class = getattr(module, class_name)

# keep the class in a corner
all_tool_classes[tool_name] = tool_class

# then create the tools object by instanciating the classes
all_tools: list[ExtendedBaseTool] = []
for name, tool_class in all_tool_classes.items():
if name in agent_config.tools:
tool_instance = tool_class.from_config(
config=agent_config.tools_library.library[name],
common_config=agent_config.common,
**({"name": name} if issubclass(tool_class, BaseLLM) else {}),
)
logger.debug("Tool %s loaded, type: %s", name, type(tool_instance))
all_tools.append(tool_instance)

return all_tools # type: ignore
148 changes: 147 additions & 1 deletion docs/docusaurus/docs/tool_library/template_tool.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,151 @@
# Template tool
Below is an example of a template tool to configure for your use case.

Here is how you can extend Agentkit Tools with your own custom tool.

## Option 1: Use an existing tools

AgentKit already provide a set of existing Tool implementations.
If your need it to simply adjust the prompts and options of an existing tools without changing the logic, you can do so by adjusting the `tools.yaml` configuration file.

For example, for a poem generator tool, you might want to adjust the prompts and options of the `BaseLLM` tool.

```yaml
library:
poem_generator:
class_name: "app.services.chat_agent.tools.library.basellm_tool.basellm_tool:BaseLLM" # reference the LLMTool, is nice starting point
description: "Generate a poem based on a given input."
prompt_message: "Generate a poem based on the following input: {question}"
system_context: "You are a poem generator. You will generate a poem based on the input."
```

Then just use the tool in your action plan as you would with any other tool.


```yaml
tools: #
- poem_generator
action_plans:
'0':
name: ''
description: Answer the user's request
actions: # each sublist is 1 action step, i.e. add tools as subitems if you want to execute in parallel
- - poem_generator
```

You can pick your implementation from the following list:

* **BaseLLM**

class_name: `app.services.chat_agent.tools.library.basellm_tool.basellm_tool:BaseLLM`

Implements the ability to use a LLM model to generate text based on configured prompts

* **SQLTool**

class_name: `app.services.chat_agent.tools.library.sql_tool.sql_tool:SQLTool`

Implements SQL database interactions and queries.

* **JsxVisualizerTool**

class_name: `app.services.chat_agent.tools.library.visualizer_tool.visualizer_tool:JsxVisualizerTool`

Enables the visualization of JSX components.

* **SummarizerTool**

class_name: `app.services.chat_agent.tools.library.summarizer_tool.summarizer_tool:SummarizerTool`

Provides functionality for summarizing text content.

* **PDFTool**

class_name: `app.services.chat_agent.tools.library.pdf_tool.pdf_tool:PDFTool`

Perform a RAG operation on textual documents.

* **ImageGenerationTool**

class_name: `app.services.chat_agent.tools.library.image_generation_tool.image_generation_tool:ImageGenerationTool`

Supports the generation of images based on specified prompt.


## Option2: Create a new tool

If you need to create a new tool, you can do so by creating a new class that extends the `ExtendedBaseTool` class.
You don't *have to* nest your tool inside the `app.services.chat_agent.tools` package, the only requirement is that the class is importable by the backend of AgentKit (i.e. it is in the python path).

Assuming you have you code in a folder called `my_tools` and the file is called `echo.py` and the `__init__.py` file along with it.

The minimal requirements for a tool are a class that extends `ExtendedBaseTool` and implements the `_arun` methods.

```python
# my_tools/echo.py
from app.services.chat_agent.tools.ExtendedBaseTool import ExtendedBaseTool

class EchoTool(ExtendedBaseTool):
# define the name of your tool, matching the name in the config [TODO: remove this constraint.]
name = "echo_tool"

async def _arun(self, query: str, run_manager: Optional[AsyncCallbackManagerForToolRun] = None) -> str:
# very simple tool that just Echoes the input back. See bellow for more complex example
return query
```

Then reference your class in the `tools.yaml` configuration file :

```yaml
library:
echo_tool:
class_name: "my_tools.echo:EchoTool" # package_name:class_name
description: "A dummy tool for testing"
prompt_message: "This is a dummy tool. You will see this message: {question}"
system_context: "You are using the dummy tool."
```

The final step is to make sure agent kit can load your package.


### For docker users

The docker that runs the backend agent is actually "Guaranted" to have the `/code` folder in the python path. So you can just volume mount your folder in the `/code` folder and it will be importable.

```yaml

# somewhere in the docker-compose-xxxx.yml you are using
services:
fastapi_server:
... # some options about the server
volumes:
... # probeably existing mounts
- ./mytools:/code/my_tools # Here, add this at the bottom of the list
```

Finally you need to down & up (restart might not be enough for a such a change) the docker container with `docker compose -f <your compose file> down` and `docker compose -f <your compose file> up -d`.



### For "on my machine" developpers

You might already run the back end with hand like with `uvicorn app.main:app` and the current folder is usually considered as part of the python path.

If not, then trouble shoot by listing the python path with `python -c "import sys; print(sys.path)"` and make sure your package is in one of the listed folders.

Adjust the PYTHONPATH environment variable with
* Linux `export PYTHONPATH=$PYTHONPATH:/path/to/your/folder`
* Windows `set PYTHONPATH=%PYTHONPATH%;C:\path\to\your\folder`

If still not, just consider using docker.



### Example of a more complex tool

The following tool is a more complex example of a tool that uses a LLM model to generate extra messages and return them as an appendix DataType.


```python
from typing import Optional, List, Tuple
from app.db.session import sql_tool_db
from langchain.schema import HumanMessage, SystemMessage
Expand All @@ -16,6 +160,8 @@ from app.services.chat_agent.helpers.llm import get_llm
from app.services.chat_agent.tools.ExtendedBaseTool import ExtendedBaseTool




class ReadMeTool(ExtendedBaseTool):
# define the name of your tool, matching the name in the config
name = "readme_tool"
Expand Down