diff --git a/Makefile b/Makefile index 0d76b4a7..3a083012 100644 --- a/Makefile +++ b/Makefile @@ -2,9 +2,8 @@ SHELL := /usr/bin/env bash OS := $(shell python -c "import sys; print(sys.platform)") # all test files define here -DEV_TEST_TOOL_FILES := ./tests/tools/test_human_feedback_tool.py ./tests/tools/test_calculator.py ./tests/tools/test_python_repl_tools.py ./tests/tools/test_sleep_tool.py ./tests/tools/test_arxiv_tools.py ./tests/tools/test_tool_manager.py +DEV_TEST_TOOL_FILES := ./tests/tools/test_human_feedback_tool.py ./tests/tools/test_calculator.py ./tests/tools/test_python_repl_tools.py ./tests/tools/test_sleep_tool.py ./tests/tools/test_arxiv_tools.py ./tests/tools/test_tool_manager.py ./tests/tools/test_file_tools.py DEV_TEST_HOOK_FILES := ./tests/hook/test_llm.py ./tests/hook/test_tool_hook.py - DEV_TEST_LLM_FILES := ./tests/llms/test_openai.py ./tests/llms/test_factory.py DEV_TEST_AGENT_FILES := ./tests/agents/test_tool_agent.py ./tests/agents/test_assistant_agent.py DEV_TEST_FILES := $(DEV_TEST_TOOL_FILES) $(DEV_TEST_HOOK_FILES) $(DEV_TEST_LLM_FILES) $(DEV_TEST_AGENT_FILES) ./tests/test_chat.py ./tests/output_formatter ./tests/test_import.py ./tests/utils/test_string_template.py @@ -20,55 +19,44 @@ else TEST_PROD_COMMAND := PYTHONPATH=$(PYTHONPATH) poetry run pytest -c pyproject.toml --cov-report=html --cov=promptulate tests endif -.PHONY: lock lock: poetry lock -n && poetry export --without-hashes > requirements.txt -.PHONY: install install: poetry install --with dev -.PHONY: install-integration install-integration: poetry install --with dev,test_integration -.PHONY: install-docs install-docs: npm i docsify-cli -g -.PHONY: pre-commit-install pre-commit-install: poetry run pre-commit install -.PHONY: polish-codestyle polish-codestyle: poetry run ruff format --config pyproject.toml promptulate tests example poetry run ruff check --fix --config pyproject.toml promptulate tests example -.PHONY: formatting formatting: polish-codestyle +format: polish-codestyle -.PHONY: test test: $(TEST_COMMAND) -.PHONY: test-prod test-prod: $(TEST_PROD_COMMAND) poetry run coverage-badge -o docs/images/coverage.svg -f -.PHONY: check-codestyle check-codestyle: poetry run ruff format --check --config pyproject.toml promptulate tests example poetry run ruff check --config pyproject.toml promptulate tests example -.PHONY: lint lint: check-codestyle test # https://github.com/Maxlinn/linn-jupyter-site-template/blob/main/.github/workflows/linn-jupyter-site-template-deploy.yml # Any notebook will be converted here. # If there are any notebook will be changed, then the notebook will be converted to markdown and pushed to the repo. -.PHONY: build-docs build-docs: jupyter nbconvert ./example/chat_usage.ipynb --to markdown --output-dir ./docs/use_cases/ jupyter nbconvert ./example/tools/custom_tool_usage.ipynb --to markdown --output-dir ./docs/modules/tools @@ -76,31 +64,46 @@ build-docs: jupyter nbconvert ./example/tools/langchain_tool_usage.ipynb --to markdown --output-dir ./docs/modules/tools jupyter nbconvert ./example/agent/assistant_agent_usage.ipynb --to markdown --output-dir ./docs/modules/agents - -.PHONY: start-docs start-docs: docsify serve docs #* Cleaning -.PHONY: pycache-remove pycache-remove: find . | grep -E "(__pycache__|\.pyc|\.pyo$$)" | xargs rm -rf -.PHONY: dsstore-remove dsstore-remove: find . | grep -E ".DS_Store" | xargs rm -rf -.PHONY: ipynbcheckpoints-remove ipynbcheckpoints-remove: find . | grep -E ".ipynb_checkpoints" | xargs rm -rf -.PHONY: pytestcache-remove pytestcache-remove: find . | grep -E ".pytest_cache" | xargs rm -rf -.PHONY: build-remove build-remove: rm -rf build/ -.PHONY: cleanup cleanup: pycache-remove dsstore-remove ipynbcheckpoints-remove pytestcache-remove + +help: + @echo "lock: Lock the dependencies and export to requirements.txt" + @echo "install: Install the dependencies" + @echo "install-integration: Install the dependencies for integration testing" + @echo "install-docs: Install the dependencies for building docs" + @echo "pre-commit-install: Install the pre-commit hooks" + @echo "polish-codestyle: Format the code" + @echo "formatting: Format the code" + @echo "test: Run the tests" + @echo "test-prod: Run the tests for production" + @echo "check-codestyle: Check the code style" + @echo "lint: Run the tests and check the code style" + @echo "build-docs: Build the docs" + @echo "start-docs: Start the docs server" + @echo "pycache-remove: Remove the pycache" + @echo "dsstore-remove: Remove the .DS_Store files" + @echo "ipynbcheckpoints-remove: Remove the ipynb checkpoints" + @echo "pytestcache-remove: Remove the pytest cache" + @echo "build-remove: Remove the build directory" + @echo "cleanup: Remove all the cache files" + +.PHONY: lock install install-integration install-docs pre-commit-install polish-codestyle formatting format test test-prod check-codestyle lint build-docs start-docs pycache-remove dsstore-remove ipynbcheckpoints-remove pytestcache-remove build-remove cleanup help \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index 6a87c4d9..0aaee97a 100644 --- a/docs/index.html +++ b/docs/index.html @@ -13,7 +13,7 @@
diff --git a/promptulate/__init__.py b/promptulate/__init__.py index 8404d819..805a79b9 100644 --- a/promptulate/__init__.py +++ b/promptulate/__init__.py @@ -1,77 +1,77 @@ -# Copyright (c) 2023 promptulate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# Copyright Owner: Zeeland -# GitHub Link: https://github.com/Undertone0809/ -# Project Link: https://github.com/Undertone0809/promptulate -# Contact Email: zeeland@foxmail.com - -import warnings - -from promptulate.agents.base import BaseAgent -from promptulate.agents.tool_agent.agent import ToolAgent -from promptulate.agents.web_agent.agent import WebAgent -from promptulate.chat import AIChat, chat -from promptulate.llms.base import BaseLLM -from promptulate.llms.factory import LLMFactory -from promptulate.llms.openai.openai import ChatOpenAI -from promptulate.output_formatter import OutputFormatter -from promptulate.schema import ( - AssistantMessage, - BaseMessage, - MessageSet, - SystemMessage, - UserMessage, -) -from promptulate.tools.base import BaseTool, Tool, define_tool -from promptulate.utils.logger import enable_log -from promptulate.utils.string_template import StringTemplate - -_util_fields = [ - "enable_log", - "OutputFormatter", - "StringTemplate", -] - -_schema_fields = [ - "AssistantMessage", - "SystemMessage", - "UserMessage", - "BaseMessage", - "MessageSet", -] - -_llm_fields = ["chat", "AIChat", "BaseLLM", "ChatOpenAI", "LLMFactory"] - -_tool_fields = [ - "Tool", - "define_tool", - "BaseTool", -] - -_agent_fields = [ - "BaseAgent", - "WebAgent", - "ToolAgent", -] - -__all__ = [ - *_util_fields, - *_schema_fields, - *_llm_fields, - *_tool_fields, - *_agent_fields, -] - -warnings.filterwarnings("always", category=DeprecationWarning) +# Copyright (c) 2023 promptulate +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Copyright Owner: Zeeland +# GitHub Link: https://github.com/Undertone0809/ +# Project Link: https://github.com/Undertone0809/promptulate +# Contact Email: zeeland@foxmail.com + +import warnings + +from promptulate.agents.base import BaseAgent +from promptulate.agents.tool_agent.agent import ToolAgent +from promptulate.agents.web_agent.agent import WebAgent +from promptulate.chat import AIChat, chat +from promptulate.llms.base import BaseLLM +from promptulate.llms.factory import LLMFactory +from promptulate.llms.openai.openai import ChatOpenAI +from promptulate.output_formatter import OutputFormatter +from promptulate.schema import ( + AssistantMessage, + BaseMessage, + MessageSet, + SystemMessage, + UserMessage, +) +from promptulate.tools.base import BaseTool, Tool, define_tool +from promptulate.utils.logger import enable_log +from promptulate.utils.string_template import StringTemplate + +_util_fields = [ + "enable_log", + "OutputFormatter", + "StringTemplate", +] + +_schema_fields = [ + "AssistantMessage", + "SystemMessage", + "UserMessage", + "BaseMessage", + "MessageSet", +] + +_llm_fields = ["chat", "AIChat", "BaseLLM", "ChatOpenAI", "LLMFactory"] + +_tool_fields = [ + "Tool", + "define_tool", + "BaseTool", +] + +_agent_fields = [ + "BaseAgent", + "WebAgent", + "ToolAgent", +] + +__all__ = [ + *_util_fields, + *_schema_fields, + *_llm_fields, + *_tool_fields, + *_agent_fields, +] + +warnings.filterwarnings("always", category=DeprecationWarning) diff --git a/promptulate/agents/tool_agent/agent.py b/promptulate/agents/tool_agent/agent.py index da9a38a6..b15b5cba 100644 --- a/promptulate/agents/tool_agent/agent.py +++ b/promptulate/agents/tool_agent/agent.py @@ -10,14 +10,14 @@ from promptulate.hook import Hook, HookTable from promptulate.llms.base import BaseLLM from promptulate.llms.openai.openai import ChatOpenAI -from promptulate.schema import ToolTypes +from promptulate.tools.base import ToolTypes from promptulate.tools.manager import ToolManager from promptulate.utils.logger import logger from promptulate.utils.string_template import StringTemplate class ActionResponse(TypedDict): - thought: str + analysis: str action_name: str action_parameters: Union[dict, str] @@ -29,7 +29,6 @@ class ToolAgent(BaseAgent): Attributes: llm (BaseLLM): The language model driver. Default is ChatOpenAI with model "gpt-3.5-turbo-16k". - stop_sequences (List[str]): The sequences that, when met, will stop the output of the llm. system_prompt_template (StringTemplate): The preset system prompt template. prefix_prompt_template (StringTemplate): The prefix system prompt template. @@ -45,6 +44,7 @@ class ToolAgent(BaseAgent): agent_goal (str): The goal of the agent. Default is "provides better assistance and services for humans.". agent_constraints (str): The constraints of the agent. Default is "none". + _from (Optional[str]): The initialization source. Default is None. """ def __init__( @@ -161,7 +161,7 @@ def _run( Hook.call_hook( HookTable.ON_AGENT_ACTION, self, - thought=action_resp["thought"], + thought=action_resp["analysis"], action=action_resp["action_name"], action_input=action_resp["action_parameters"], ) @@ -211,7 +211,7 @@ def _parse_llm_response(self, llm_resp: str) -> ActionResponse: data: dict = json.loads(llm_resp) return ActionResponse( - thought=data["thought"], + analysis=data["analysis"], action_name=data["action"]["name"], action_parameters=data["action"]["args"], ) diff --git a/promptulate/agents/tool_agent/prompt.py b/promptulate/agents/tool_agent/prompt.py index d9b0564a..c0f3fd92 100644 --- a/promptulate/agents/tool_agent/prompt.py +++ b/promptulate/agents/tool_agent/prompt.py @@ -41,8 +41,7 @@ ```json { -"thought": "The thought of what to do and why.", -"self_criticism":"Constructive self-criticism of the thought", +"analysis": "The thought of what to do and why.", "action": # the action to take, must be one of provided tools { "name": "tool name", @@ -61,10 +60,11 @@ to answer the question without using any more tools. At that point, you MUST respond in the one of the following two formats: +- If you can answer the question: + ```json { -"thought": "The thought of what to do and why.", -"self_criticism":"Constructive self-criticism of the thought", +"analysis": "The thought of what to do and why.", "action": { "name": "finish", "args": {"content": "You answer here."} @@ -72,10 +72,11 @@ } ``` +- If you cannot answer the question in the current context: + ```json { "thought": "The thought of what to do and why.", -"self_criticism":"Constructive self-criticism of the thought", "action": { "name": "finish", "args": {"content": "Sorry, I cannot answer your query, because (Summary all the upper steps, and explain)"} diff --git a/promptulate/beta/__init__.py b/promptulate/beta/__init__.py index e69de29b..2786e4c7 100644 --- a/promptulate/beta/__init__.py +++ b/promptulate/beta/__init__.py @@ -0,0 +1,3 @@ +from promptulate.beta import agents, rag + +__all__ = ["agents", "rag"] diff --git a/promptulate/beta/agents/assistant_agent/agent.py b/promptulate/beta/agents/assistant_agent/agent.py index 75c26933..3c750c6c 100644 --- a/promptulate/beta/agents/assistant_agent/agent.py +++ b/promptulate/beta/agents/assistant_agent/agent.py @@ -11,7 +11,7 @@ from promptulate.beta.agents.assistant_agent.schema import Plan from promptulate.hook import Hook, HookTable from promptulate.llms.base import BaseLLM -from promptulate.schema import ToolTypes +from promptulate.tools.base import ToolTypes from promptulate.tools.manager import ToolManager from promptulate.utils.logger import logger diff --git a/promptulate/chat.py b/promptulate/chat.py index caaffa9f..b96cc974 100644 --- a/promptulate/chat.py +++ b/promptulate/chat.py @@ -14,9 +14,8 @@ BaseMessage, MessageSet, StreamIterator, - ToolTypes, ) -from promptulate.tools.base import BaseTool +from promptulate.tools.base import BaseTool, ToolTypes from promptulate.utils.logger import logger T = TypeVar("T", bound=BaseModel) diff --git a/promptulate/schema.py b/promptulate/schema.py index 56eb2cf3..cad3ee40 100644 --- a/promptulate/schema.py +++ b/promptulate/schema.py @@ -13,16 +13,9 @@ "AssistantMessage", "MessageSet", "init_chat_message_history", - "ToolTypes", "StreamIterator", ] -if TYPE_CHECKING: - from langchain.tools.base import BaseTool as LangchainBaseToolType # noqa - from promptulate.tools.base import BaseTool, Tool # noqa - -ToolTypes = Union["BaseTool", "Tool", Callable, "LangchainBaseToolType"] - class BaseMessage(BaseModel): """Message basic object.""" diff --git a/promptulate/tools/base.py b/promptulate/tools/base.py index c68c6499..cfa7243a 100644 --- a/promptulate/tools/base.py +++ b/promptulate/tools/base.py @@ -1,7 +1,7 @@ import inspect import warnings from abc import ABC, abstractmethod -from typing import Any, Callable, Dict, List, Optional, Type, Union +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Type, Union from promptulate.hook.base import Hook, HookTable from promptulate.pydantic_v1 import ( @@ -12,6 +12,11 @@ ) from promptulate.utils.logger import logger +if TYPE_CHECKING: + from langchain.tools.base import BaseTool as LangchainBaseToolType # noqa + +ToolTypes = Union["BaseTool", "Tool", Callable, "LangchainBaseToolType", "BaseToolKit"] + class _SchemaConfig: """Configuration for the pydantic model.""" @@ -381,5 +386,6 @@ def function_to_tool(func: Callable) -> ToolImpl: class BaseToolKit: @abstractmethod - def get_tools(self): + def get_tools(self) -> List[ToolTypes]: """get tools in the toolkit""" + raise NotImplementedError diff --git a/promptulate/tools/file/toolkit.py b/promptulate/tools/file/toolkit.py index f30c41e5..61bdc6b7 100644 --- a/promptulate/tools/file/toolkit.py +++ b/promptulate/tools/file/toolkit.py @@ -1,7 +1,7 @@ import os -from typing import List, Optional +from typing import List, Literal, Optional -from promptulate.tools.base import BaseToolKit, Tool +from promptulate.tools.base import BaseToolKit, ToolTypes from promptulate.tools.file.tools import ( AppendFileTool, CopyFileTool, @@ -12,6 +12,7 @@ WriteFileTool, ) +FileToolType = Literal["write", "append", "read", "delete", "list", "copy", "move"] TOOL_MAPPER = { "write": WriteFileTool, "append": AppendFileTool, @@ -28,13 +29,18 @@ class FileToolKit(BaseToolKit): Args: root_dir: The root directory of the file tool. - selected_tools: The selected tools of the file tool. + modes(Option): The modes of the file tool. Default is None. Returns: The instance object of the corresponding tool """ - def __init__(self, root_dir: str = None, modes: Optional[List[str]] = None) -> None: + def __init__( + self, + *, + root_dir: Optional[str] = None, + modes: Optional[List[FileToolType]] = None, + ) -> None: self.root_dir = root_dir or os.getcwd() self.modes = modes or [] @@ -45,7 +51,7 @@ def __init__(self, root_dir: str = None, modes: Optional[List[str]] = None) -> N f"Please select from {list(TOOL_MAPPER.keys())}" ) - def get_tools(self) -> List[Tool]: + def get_tools(self) -> List[ToolTypes]: if self.modes: return [TOOL_MAPPER[mode](self.root_dir) for mode in self.modes] return [tool(self.root_dir) for tool in TOOL_MAPPER.values()] diff --git a/promptulate/tools/manager.py b/promptulate/tools/manager.py index 16a1fb3a..d8604e6d 100644 --- a/promptulate/tools/manager.py +++ b/promptulate/tools/manager.py @@ -2,12 +2,18 @@ import json from typing import Any, List, Optional, Union -from promptulate.schema import ToolTypes -from promptulate.tools.base import BaseTool, Tool, ToolImpl, function_to_tool +from promptulate.tools.base import ( + BaseTool, + BaseToolKit, + Tool, + ToolImpl, + ToolTypes, + function_to_tool, +) from promptulate.tools.langchain.tools import LangchainTool -def _judge_langchain_tool_and_wrap(tool: Any) -> Optional[Tool]: +def _judge_langchain_tool_and_wrap(tool: Any) -> Tool: """Judge if the tool is a langchain tool and wrap it. Args: @@ -22,6 +28,7 @@ def _judge_langchain_tool_and_wrap(tool: Any) -> Optional[Tool]: if isinstance(tool, LangchainBaseTool): return LangchainTool(tool) + raise ValueError(f"Unknown tool type {tool}.") except ImportError: raise ValueError( ( @@ -31,7 +38,7 @@ def _judge_langchain_tool_and_wrap(tool: Any) -> Optional[Tool]: ) -def _initialize_tool(tool: ToolTypes) -> Optional[Tool]: +def _initialize_tool(tool: ToolTypes) -> Union[Tool, List[Tool]]: """Initialize the tool. Args: @@ -41,6 +48,12 @@ def _initialize_tool(tool: ToolTypes) -> Optional[Tool]: Returns: Optional[Tool]: The initialized tool. """ + if isinstance(tool, BaseToolKit): + initialized_tools = [] + for tool in tool.get_tools(): + initialized_tools.append(_initialize_tool(tool)) + return initialized_tools + if isinstance(tool, BaseTool): return ToolImpl.from_base_tool(tool) elif isinstance(tool, Tool): @@ -55,11 +68,15 @@ class ToolManager: """ToolManager helps Agent to manage tools""" def __init__(self, tools: List[ToolTypes]): - self.tools: List[Tool] = [ - _initialize_tool(tool) - for tool in tools - if _initialize_tool(tool) is not None - ] + self.tools: List[Tool] = [] + + for tool in tools: + initialized_tool: Union[list, Tool] = _initialize_tool(tool) + + if isinstance(initialized_tool, list): + self.tools.extend(initialized_tool) + else: + self.tools.append(initialized_tool) def get_tool(self, tool_name: str) -> Optional[Tool]: """Find specified tool by tool name. diff --git a/pyproject.toml b/pyproject.toml index 9c5b4bb9..c6c1d74f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ description = "A powerful LLM Application development framework." name = "promptulate" readme = "README.md" repository = "https://github.com/Undertone0809/promptulate" -version = "1.15.0" +version = "1.15.1" keywords = [ "promptulate", "pne", diff --git a/tests/agents/test_tool_agent.py b/tests/agents/test_tool_agent.py index 1b0e787f..7b35230b 100644 --- a/tests/agents/test_tool_agent.py +++ b/tests/agents/test_tool_agent.py @@ -1,5 +1,6 @@ from promptulate.agents.tool_agent.agent import ToolAgent from promptulate.llms.base import BaseLLM +from promptulate.tools.base import BaseToolKit class FakeLLM(BaseLLM): @@ -35,3 +36,20 @@ def test_init(): assert len(agent.tool_manager.tools) == 2 assert agent.tool_manager.tools[0].name == "fake_tool_1" assert agent.tool_manager.tools[1].name == "fake_tool_2" + + +class MockToolKit(BaseToolKit): + def get_tools(self) -> list: + return [fake_tool_1, fake_tool_2] + + +def test_init_by_toolkits(): + llm = FakeLLM() + agent = ToolAgent(llm=llm, tools=[MockToolKit()]) + assert len(agent.tool_manager.tools) == 2 + + +def test_init_by_tool_and_kit(): + llm = FakeLLM() + agent = ToolAgent(llm=llm, tools=[MockToolKit(), fake_tool_1, fake_tool_2]) + assert len(agent.tool_manager.tools) == 4