From 9184ed0b9b48673d6c324202d91ef9b6ff8ca654 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Thu, 20 Jun 2024 00:28:54 +0900 Subject: [PATCH 01/32] feat: Add ToolKit for new interface --- mirascope/core/_internal/utils.py | 25 +++++--- mirascope/core/base/__init__.py | 11 +++- mirascope/core/base/toolkit.py | 87 ++++++++++++++++++++++++++++ mirascope/core/openai/openai_call.py | 11 ++++ 4 files changed, 126 insertions(+), 8 deletions(-) create mode 100644 mirascope/core/base/toolkit.py diff --git a/mirascope/core/_internal/utils.py b/mirascope/core/_internal/utils.py index 51e56afc..3c2b420f 100644 --- a/mirascope/core/_internal/utils.py +++ b/mirascope/core/_internal/utils.py @@ -28,15 +28,17 @@ """ -def format_prompt_template(template: str, attrs: dict[str, Any]) -> str: - """Formats the given prompt `template`""" - dedented_template = dedent(template).strip() - template_vars = [ - var for _, var, _, _ in Formatter().parse(dedented_template) if var is not None - ] +def get_template_variables(template: str) -> list[str]: + """Returns the variables in the given template string.""" + return [var for _, var, _, _ in Formatter().parse(template) if var is not None] + +def get_template_values( + template_variables: list[str], attrs: dict[str, Any] +) -> dict[str, Any]: + """Returns the values of the given `template_variables` from the provided `attrs`.""" values = {} - for var in template_vars: + for var in template_variables: attr = attrs[var] if isinstance(attr, list): if len(attr) == 0: @@ -49,6 +51,15 @@ def format_prompt_template(template: str, attrs: dict[str, Any]) -> str: values[var] = "\n".join([str(item) for item in attr]) else: values[var] = str(attr) + return values + + +def format_prompt_template(template: str, attrs: dict[str, Any]) -> str: + """Formats the given prompt `template`""" + dedented_template = dedent(template).strip() + template_vars = get_template_variables(dedented_template) + + values = get_template_values(template_vars, attrs) return dedented_template.format(**values) diff --git a/mirascope/core/base/__init__.py b/mirascope/core/base/__init__.py index cfd4de3e..732d1d53 100644 --- a/mirascope/core/base/__init__.py +++ b/mirascope/core/base/__init__.py @@ -3,5 +3,14 @@ from .prompt import BasePrompt, tags from .tool import BaseTool from .types import BaseCallResponse, MessageParam +from .toolkit import BaseToolKit, toolkit_tool -__all__ = ["BaseCallResponse", "BasePrompt", "BaseTool", "MessageParam", "tags"] +__all__ = [ + "BaseCallResponse", + "BasePrompt", + "BaseTool", + "MessageParam", + "tags", + "BaseToolKit", + "toolkit_tool", +] diff --git a/mirascope/core/base/toolkit.py b/mirascope/core/base/toolkit.py new file mode 100644 index 00000000..4927008f --- /dev/null +++ b/mirascope/core/base/toolkit.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +from abc import ABC +from textwrap import dedent +from typing import Callable, Optional, ClassVar, ParamSpec +from functools import wraps + +from pydantic import BaseModel, ConfigDict +from typing_extensions import LiteralString + +from .tool import BaseTool +from .._internal.utils import get_template_variables + +_TOOLKIT_TOOL_METHOD_MARKER: LiteralString = "__toolkit_tool_method__" + +P = ParamSpec("P") + + +def toolkit_tool( + method: Callable[[BaseToolKit, ...], str], +) -> Callable[[BaseToolKit, ...], str]: + # Mark the method as a toolkit tool + setattr(method, _TOOLKIT_TOOL_METHOD_MARKER, True) + + # TODO: Validate first argument is self + @wraps(method) + def inner(*args, **kwargs): + return method(*args, **kwargs) + + return inner + + +class BaseToolKit(BaseModel, ABC): + """A class for defining tools for LLM call tools.""" + + model_config = ConfigDict(arbitrary_types_allowed=True) + _toolkit_tool_method: ClassVar[Callable[..., str]] + _toolkit_template_vars: ClassVar[list[str]] + + def create_tool(self) -> type[BaseTool]: + """The method to create the tools.""" + formated_template = self._toolkit_tool_method.__doc__.format( + **{var: getattr(self, var) for var in self._toolkit_template_vars} + ) + # TODO: Generate the tool class with the formatted template + ... + + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + toolkit_tool_method: Optional[Callable] = None + for name, value in cls.__dict__.items(): + if getattr(value, _TOOLKIT_TOOL_METHOD_MARKER, False): + if toolkit_tool_method: + raise ValueError("Only one toolkit_tool method is allowed") + toolkit_tool_method = value + if not toolkit_tool_method: + raise ValueError("No toolkit_tool method found") + + # Validate the toolkit_tool_method + if (template := toolkit_tool_method.__doc__) is None: + raise ValueError("The toolkit_tool method must have a docstring") + + dedented_template = dedent(template).strip() + if not (template_vars := get_template_variables(dedented_template)): + raise ValueError("The toolkit_tool method must have template variables") + + if dedented_template != template: + toolkit_tool_method.__doc__ = dedented_template + + for var in template_vars: + if not var.startswith("self."): + # Should be supported un-self variables? + raise ValueError( + "The toolkit_tool method must use self. prefix in template variables" + ) + + self_var = var[5:] + # Expecting pydantic model fields or class attribute and property + # TODO: Check attribute type such like callable, property, etc. + if self_var in cls.model_fields_set or hasattr(cls, self_var): + continue + raise ValueError( + f"The toolkit_tool method template variable {var} is not found in the class" + ) + + cls._toolkit_tool_method = toolkit_tool_method + cls._toolkit_template_vars = template_vars diff --git a/mirascope/core/openai/openai_call.py b/mirascope/core/openai/openai_call.py index 5e40ce55..a06efc84 100644 --- a/mirascope/core/openai/openai_call.py +++ b/mirascope/core/openai/openai_call.py @@ -42,6 +42,17 @@ def decorator(fn: Callable[P, R]) -> Callable[P, OpenAICallResponse]: def call(*args: P.args, **kwargs: P.kwargs) -> OpenAICallResponse: prompt_template = inspect.getdoc(fn) assert prompt_template is not None, "The function must have a docstring." + + # Try to get the dictionary for tools from the function result + # tools = [] + fn_result = fn(*args, **kwargs) + if isinstance(fn_result, dict): + if fn_result_tools := fn_result.get("tools"): + for fn_result_tool in fn_result_tools: + # TODO: Generate tools + # tools.append(generate(fn_result_tool)) + pass + attrs = inspect.signature(fn).bind(*args, **kwargs).arguments messages = utils.parse_prompt_messages( roles=["system", "user", "assistant"], From 83ce746cc1a95746452f00e4faaac7785c16398d Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Thu, 20 Jun 2024 09:37:45 +0900 Subject: [PATCH 02/32] Update BaseTool import --- mirascope/core/base/toolkit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mirascope/core/base/toolkit.py b/mirascope/core/base/toolkit.py index 4927008f..36ac036f 100644 --- a/mirascope/core/base/toolkit.py +++ b/mirascope/core/base/toolkit.py @@ -8,7 +8,7 @@ from pydantic import BaseModel, ConfigDict from typing_extensions import LiteralString -from .tool import BaseTool +from . import BaseTool from .._internal.utils import get_template_variables _TOOLKIT_TOOL_METHOD_MARKER: LiteralString = "__toolkit_tool_method__" From d2afc1da4091167fa4d4035c6e200d76487f1315 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Fri, 21 Jun 2024 01:37:27 +0900 Subject: [PATCH 03/32] implement create_tool --- mirascope/core/_internal/utils.py | 10 ++++++---- mirascope/core/base/toolkit.py | 7 +++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/mirascope/core/_internal/utils.py b/mirascope/core/_internal/utils.py index 5c97c953..1e5ccbf9 100644 --- a/mirascope/core/_internal/utils.py +++ b/mirascope/core/_internal/utils.py @@ -110,7 +110,7 @@ def parse_prompt_messages( def convert_function_to_base_tool( - fn: Callable, base: type[BaseToolT] + fn: Callable, base: type[BaseToolT], __doc__: str | None = None ) -> type[BaseToolT]: """Constructst a `BaseToolT` type from the given function. @@ -121,6 +121,7 @@ def convert_function_to_base_tool( Args: fn: The function to convert. base: The `BaseToolT` type to which the function is converted. + __doc__: The docstring to use for the constructed `BaseToolT` type. Returns: The constructed `BaseToolT` type. @@ -133,8 +134,9 @@ def convert_function_to_base_tool( doesn't have a docstring description. """ docstring = None - if fn.__doc__: - docstring = parse(fn.__doc__) + func_doc = __doc__ or fn.__doc__ + if func_doc: + docstring = parse(func_doc) field_definitions = {} hints = get_type_hints(fn) @@ -178,7 +180,7 @@ def convert_function_to_base_tool( model = create_model( fn.__name__, __base__=base, - __doc__=inspect.cleandoc(fn.__doc__) if fn.__doc__ else DEFAULT_TOOL_DOCSTRING, + __doc__=inspect.cleandoc(func_doc) if func_doc else DEFAULT_TOOL_DOCSTRING, **cast(dict[str, Any], field_definitions), ) diff --git a/mirascope/core/base/toolkit.py b/mirascope/core/base/toolkit.py index 36ac036f..4e31e933 100644 --- a/mirascope/core/base/toolkit.py +++ b/mirascope/core/base/toolkit.py @@ -9,7 +9,7 @@ from typing_extensions import LiteralString from . import BaseTool -from .._internal.utils import get_template_variables +from .._internal.utils import get_template_variables, convert_function_to_base_tool _TOOLKIT_TOOL_METHOD_MARKER: LiteralString = "__toolkit_tool_method__" @@ -34,7 +34,7 @@ class BaseToolKit(BaseModel, ABC): """A class for defining tools for LLM call tools.""" model_config = ConfigDict(arbitrary_types_allowed=True) - _toolkit_tool_method: ClassVar[Callable[..., str]] + _toolkit_tool_method: ClassVar[Callable[[BaseToolKit, ...], str]] _toolkit_template_vars: ClassVar[list[str]] def create_tool(self) -> type[BaseTool]: @@ -42,8 +42,7 @@ def create_tool(self) -> type[BaseTool]: formated_template = self._toolkit_tool_method.__doc__.format( **{var: getattr(self, var) for var in self._toolkit_template_vars} ) - # TODO: Generate the tool class with the formatted template - ... + return convert_function_to_base_tool(self._toolkit_tool_method, BaseTool, formated_template) def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) From 1c8d0cb31255ba07b9a95612ef3d9c2e3b4eefbe Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Fri, 21 Jun 2024 01:39:16 +0900 Subject: [PATCH 04/32] Fix the order in `__all__` --- mirascope/core/base/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mirascope/core/base/__init__.py b/mirascope/core/base/__init__.py index 4b3f1ad9..5f451fea 100644 --- a/mirascope/core/base/__init__.py +++ b/mirascope/core/base/__init__.py @@ -20,7 +20,7 @@ "BasePrompt", "BaseStream", "BaseTool", - "tags", "BaseToolKit", + "tags", "toolkit_tool" ] From 30cdb19bc9f49f1f547a08810a8712698611b0f7 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Fri, 21 Jun 2024 01:46:37 +0900 Subject: [PATCH 05/32] fix toolkit_tool decorator --- mirascope/core/base/toolkit.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/mirascope/core/base/toolkit.py b/mirascope/core/base/toolkit.py index 4e31e933..dd662b83 100644 --- a/mirascope/core/base/toolkit.py +++ b/mirascope/core/base/toolkit.py @@ -3,7 +3,6 @@ from abc import ABC from textwrap import dedent from typing import Callable, Optional, ClassVar, ParamSpec -from functools import wraps from pydantic import BaseModel, ConfigDict from typing_extensions import LiteralString @@ -22,12 +21,7 @@ def toolkit_tool( # Mark the method as a toolkit tool setattr(method, _TOOLKIT_TOOL_METHOD_MARKER, True) - # TODO: Validate first argument is self - @wraps(method) - def inner(*args, **kwargs): - return method(*args, **kwargs) - - return inner + return method class BaseToolKit(BaseModel, ABC): From aee68bc679c67a4f0a1bf48826cafe517cf058c1 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Fri, 21 Jun 2024 01:57:41 +0900 Subject: [PATCH 06/32] Add unittest --- tests/core/base/test_toolkit.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 tests/core/base/test_toolkit.py diff --git a/tests/core/base/test_toolkit.py b/tests/core/base/test_toolkit.py new file mode 100644 index 00000000..cc9c9d08 --- /dev/null +++ b/tests/core/base/test_toolkit.py @@ -0,0 +1,24 @@ +"""Tests for the `toolkit` module.""" +from typing import Literal + +from mirascope.core.base import BaseToolKit, toolkit_tool + + +def test_toolkit() -> None: + """Tests the `BaseToolKit` class and the `toolkit_tool` decorator.""" + + class BookRecommendationToolKit(BaseToolKit): + """A toolkit for recommending books.""" + + reading_level: Literal["beginner", "advanced"] + + @toolkit_tool + def format_book(self, title: str, author: str) -> str: + """Returns the title and author of a book nicely formatted. + + Reading level: {self.reading_level} + """ + return f"{title} by {author}" + + toolkit = BookRecommendationToolKit(reading_level="beginner") + tool = toolkit.create_tool() From 32f592bdd335d19805c9b0607099915fa5cbd400 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Fri, 21 Jun 2024 02:32:22 +0900 Subject: [PATCH 07/32] Fix toolkit logic --- mirascope/core/base/toolkit.py | 12 ++++-------- tests/core/base/test_toolkit.py | 1 + 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/mirascope/core/base/toolkit.py b/mirascope/core/base/toolkit.py index dd662b83..235fc7a0 100644 --- a/mirascope/core/base/toolkit.py +++ b/mirascope/core/base/toolkit.py @@ -33,13 +33,11 @@ class BaseToolKit(BaseModel, ABC): def create_tool(self) -> type[BaseTool]: """The method to create the tools.""" - formated_template = self._toolkit_tool_method.__doc__.format( - **{var: getattr(self, var) for var in self._toolkit_template_vars} - ) + formated_template = self._toolkit_tool_method.__doc__.format(self=self) return convert_function_to_base_tool(self._toolkit_tool_method, BaseTool, formated_template) - def __init_subclass__(cls, **kwargs): - super().__init_subclass__(**kwargs) + @classmethod + def __pydantic_init_subclass__(cls, **kwargs): toolkit_tool_method: Optional[Callable] = None for name, value in cls.__dict__.items(): if getattr(value, _TOOLKIT_TOOL_METHOD_MARKER, False): @@ -59,7 +57,6 @@ def __init_subclass__(cls, **kwargs): if dedented_template != template: toolkit_tool_method.__doc__ = dedented_template - for var in template_vars: if not var.startswith("self."): # Should be supported un-self variables? @@ -69,8 +66,7 @@ def __init_subclass__(cls, **kwargs): self_var = var[5:] # Expecting pydantic model fields or class attribute and property - # TODO: Check attribute type such like callable, property, etc. - if self_var in cls.model_fields_set or hasattr(cls, self_var): + if self_var in cls.model_fields or hasattr(cls, self_var): continue raise ValueError( f"The toolkit_tool method template variable {var} is not found in the class" diff --git a/tests/core/base/test_toolkit.py b/tests/core/base/test_toolkit.py index cc9c9d08..a3e16076 100644 --- a/tests/core/base/test_toolkit.py +++ b/tests/core/base/test_toolkit.py @@ -22,3 +22,4 @@ def format_book(self, title: str, author: str) -> str: toolkit = BookRecommendationToolKit(reading_level="beginner") tool = toolkit.create_tool() + assert tool.__doc__ == "Returns the title and author of a book nicely formatted.\n\nReading level: beginner" From ddab43002f8109e6076c262edf303380b19aa765 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Fri, 21 Jun 2024 02:51:31 +0900 Subject: [PATCH 08/32] Update unittest --- tests/core/base/test_toolkit.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/core/base/test_toolkit.py b/tests/core/base/test_toolkit.py index a3e16076..c1f53381 100644 --- a/tests/core/base/test_toolkit.py +++ b/tests/core/base/test_toolkit.py @@ -22,4 +22,7 @@ def format_book(self, title: str, author: str) -> str: toolkit = BookRecommendationToolKit(reading_level="beginner") tool = toolkit.create_tool() - assert tool.__doc__ == "Returns the title and author of a book nicely formatted.\n\nReading level: beginner" + assert tool.name() == "format_book" + assert tool.description() == "Returns the title and author of a book nicely formatted.\n\nReading level: beginner" + assert tool(title="The Name of the Wind", + author="Rothfuss, Patrick").call() == "The Name of the Wind by Rothfuss, Patrick" From b6425b508ee514371335c672fc23685ac3933f46 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Fri, 21 Jun 2024 10:02:00 +0900 Subject: [PATCH 09/32] Fix unittest --- mirascope/core/base/toolkit.py | 2 +- tests/core/base/test_toolkit.py | 4 ++-- tests/core/base/test_utils.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mirascope/core/base/toolkit.py b/mirascope/core/base/toolkit.py index 235fc7a0..5a56970a 100644 --- a/mirascope/core/base/toolkit.py +++ b/mirascope/core/base/toolkit.py @@ -8,7 +8,7 @@ from typing_extensions import LiteralString from . import BaseTool -from .._internal.utils import get_template_variables, convert_function_to_base_tool +from ._utils import convert_function_to_base_tool, get_template_variables _TOOLKIT_TOOL_METHOD_MARKER: LiteralString = "__toolkit_tool_method__" diff --git a/tests/core/base/test_toolkit.py b/tests/core/base/test_toolkit.py index c1f53381..df2f4588 100644 --- a/tests/core/base/test_toolkit.py +++ b/tests/core/base/test_toolkit.py @@ -22,7 +22,7 @@ def format_book(self, title: str, author: str) -> str: toolkit = BookRecommendationToolKit(reading_level="beginner") tool = toolkit.create_tool() - assert tool.name() == "format_book" - assert tool.description() == "Returns the title and author of a book nicely formatted.\n\nReading level: beginner" + assert tool._name() == "format_book" + assert tool._description() == "Returns the title and author of a book nicely formatted.\n\nReading level: beginner" assert tool(title="The Name of the Wind", author="Rothfuss, Patrick").call() == "The Name of the Wind by Rothfuss, Patrick" diff --git a/tests/core/base/test_utils.py b/tests/core/base/test_utils.py index 989fcc52..989450c0 100644 --- a/tests/core/base/test_utils.py +++ b/tests/core/base/test_utils.py @@ -6,7 +6,7 @@ import pytest from pydantic import BaseModel -from mirascope.core.base import BaseTool, _utils +from mirascope.core.base import _utils, BaseTool def test_format_prompt_template() -> None: From 3781a3eedfb49f307a0c1fd99128f3a848a185e6 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Fri, 21 Jun 2024 23:28:40 +0900 Subject: [PATCH 10/32] Apply suggestions --- mirascope/core/base/_utils.py | 30 +++++++------ mirascope/core/base/prompts.py | 2 +- mirascope/core/base/toolkit.py | 79 ++++++++++++++++++--------------- tests/core/base/test_toolkit.py | 15 +++++-- tests/core/base/test_utils.py | 5 +-- 5 files changed, 73 insertions(+), 58 deletions(-) diff --git a/mirascope/core/base/_utils.py b/mirascope/core/base/_utils.py index cf3907a0..2157741d 100644 --- a/mirascope/core/base/_utils.py +++ b/mirascope/core/base/_utils.py @@ -5,7 +5,6 @@ from abc import update_abstractmethods from enum import Enum from string import Formatter -from textwrap import dedent from typing import ( Annotated, Any, @@ -62,9 +61,9 @@ def get_template_values( return values -def format_prompt_template(template: str, attrs: dict[str, Any]) -> str: +def format_template(template: str, attrs: dict[str, Any]) -> str: """Formats the given prompt `template`""" - dedented_template = dedent(template).strip() + dedented_template = inspect.cleandoc(template).strip() template_vars = get_template_variables(dedented_template) values = get_template_values(template_vars, attrs) @@ -105,14 +104,14 @@ def parse_prompt_messages( ) messages += attr else: - content = format_prompt_template(match.group(2), attrs) + content = format_template(match.group(2), attrs) if content: messages.append({"role": role, "content": content}) if len(messages) == 0: messages.append( { "role": "user", - "content": format_prompt_template(template, attrs), + "content": format_template(template, attrs), } ) return messages @@ -154,8 +153,10 @@ def convert_function_to_base_tool( field_definitions = {} hints = get_type_hints(fn) + has_self_or_cls = False for i, parameter in enumerate(inspect.signature(fn).parameters.values()): if parameter.name == "self" or parameter.name == "cls": + has_self_or_cls = True continue if parameter.annotation == inspect.Parameter.empty: raise ValueError("All parameters must have a type annotation.") @@ -200,14 +201,17 @@ def convert_function_to_base_tool( def call(self: base): return fn( - **{ - str( - self.model_fields[field_name].alias - if self.model_fields[field_name].alias - else field_name - ): getattr(self, field_name) - for field_name in self.model_dump(exclude={"tool_call"}) - } + **( + ({"self": self} if has_self_or_cls else {}) + | { + str( + self.model_fields[field_name].alias + if self.model_fields[field_name].alias + else field_name + ): getattr(self, field_name) + for field_name in self.model_dump(exclude={"tool_call"}) + } + ) ) setattr(model, "call", call) diff --git a/mirascope/core/base/prompts.py b/mirascope/core/base/prompts.py index 1643a4d9..8a74ea28 100644 --- a/mirascope/core/base/prompts.py +++ b/mirascope/core/base/prompts.py @@ -41,7 +41,7 @@ class BookRecommendationPrompt(BasePrompt): def __str__(self) -> str: """Returns the formatted template.""" - return _utils.format_prompt_template(self.prompt_template, self.model_dump()) + return _utils.format_template(self.prompt_template, self.model_dump()) def message_params(self) -> list[BaseMessageParam]: """Returns the template as a formatted list of `Message` instances.""" diff --git a/mirascope/core/base/toolkit.py b/mirascope/core/base/toolkit.py index 5a56970a..60d9bfc5 100644 --- a/mirascope/core/base/toolkit.py +++ b/mirascope/core/base/toolkit.py @@ -1,8 +1,8 @@ from __future__ import annotations +import inspect from abc import ABC -from textwrap import dedent -from typing import Callable, Optional, ClassVar, ParamSpec +from typing import Callable, ClassVar, ParamSpec, NamedTuple from pydantic import BaseModel, ConfigDict from typing_extensions import LiteralString @@ -16,7 +16,7 @@ def toolkit_tool( - method: Callable[[BaseToolKit, ...], str], + method: Callable[[BaseToolKit, ...], str], ) -> Callable[[BaseToolKit, ...], str]: # Mark the method as a toolkit tool setattr(method, _TOOLKIT_TOOL_METHOD_MARKER, True) @@ -24,53 +24,58 @@ def toolkit_tool( return method +class TookKitToolMethod(NamedTuple): + method: Callable[[BaseToolKit, ...], str] + template_vars: list[str] + template: str + + class BaseToolKit(BaseModel, ABC): """A class for defining tools for LLM call tools.""" model_config = ConfigDict(arbitrary_types_allowed=True) - _toolkit_tool_method: ClassVar[Callable[[BaseToolKit, ...], str]] - _toolkit_template_vars: ClassVar[list[str]] + _toolkit_tool_methods: ClassVar[list[TookKitToolMethod]] - def create_tool(self) -> type[BaseTool]: + def create_tools(self) -> list[type[BaseTool]]: """The method to create the tools.""" - formated_template = self._toolkit_tool_method.__doc__.format(self=self) - return convert_function_to_base_tool(self._toolkit_tool_method, BaseTool, formated_template) + tools: list[type[BaseTool]] = [] + for method, template_vars, template in self._toolkit_tool_methods: + formatted_template = template.format(self=self) + tool = convert_function_to_base_tool(method, BaseTool, formatted_template) + tools.append(tool) + return tools @classmethod def __pydantic_init_subclass__(cls, **kwargs): - toolkit_tool_method: Optional[Callable] = None - for name, value in cls.__dict__.items(): - if getattr(value, _TOOLKIT_TOOL_METHOD_MARKER, False): - if toolkit_tool_method: - raise ValueError("Only one toolkit_tool method is allowed") - toolkit_tool_method = value - if not toolkit_tool_method: - raise ValueError("No toolkit_tool method found") + cls._toolkit_tool_methods = [] + for attr in cls.__dict__.values(): + if not getattr(attr, _TOOLKIT_TOOL_METHOD_MARKER, False): + continue + # Validate the toolkit_tool_method + if (template := attr.__doc__) is None: + raise ValueError("The toolkit_tool method must have a docstring") - # Validate the toolkit_tool_method - if (template := toolkit_tool_method.__doc__) is None: - raise ValueError("The toolkit_tool method must have a docstring") + dedented_template = inspect.cleandoc(template) + template_vars = get_template_variables(dedented_template) - dedented_template = dedent(template).strip() - if not (template_vars := get_template_variables(dedented_template)): - raise ValueError("The toolkit_tool method must have template variables") + for var in template_vars: + if not var.startswith("self."): + # Should be supported un-self variables? + raise ValueError( + "The toolkit_tool method must use self. prefix in template variables" + ) - if dedented_template != template: - toolkit_tool_method.__doc__ = dedented_template - for var in template_vars: - if not var.startswith("self."): - # Should be supported un-self variables? + self_var = var[5:] + + # Expecting pydantic model fields or class attribute and property + if self_var in cls.model_fields or hasattr(cls, self_var): + continue raise ValueError( - "The toolkit_tool method must use self. prefix in template variables" + f"The toolkit_tool method template variable {var} is not found in the class" ) - self_var = var[5:] - # Expecting pydantic model fields or class attribute and property - if self_var in cls.model_fields or hasattr(cls, self_var): - continue - raise ValueError( - f"The toolkit_tool method template variable {var} is not found in the class" + cls._toolkit_tool_methods.append( + TookKitToolMethod(attr, template_vars, dedented_template) ) - - cls._toolkit_tool_method = toolkit_tool_method - cls._toolkit_template_vars = template_vars + if not cls._toolkit_tool_methods: + raise ValueError("No toolkit_tool method found") diff --git a/tests/core/base/test_toolkit.py b/tests/core/base/test_toolkit.py index df2f4588..a7c768e9 100644 --- a/tests/core/base/test_toolkit.py +++ b/tests/core/base/test_toolkit.py @@ -21,8 +21,15 @@ def format_book(self, title: str, author: str) -> str: return f"{title} by {author}" toolkit = BookRecommendationToolKit(reading_level="beginner") - tool = toolkit.create_tool() + tools = toolkit.create_tools() + assert len(tools) == 1 + tool = tools[0] assert tool._name() == "format_book" - assert tool._description() == "Returns the title and author of a book nicely formatted.\n\nReading level: beginner" - assert tool(title="The Name of the Wind", - author="Rothfuss, Patrick").call() == "The Name of the Wind by Rothfuss, Patrick" + assert ( + tool._description() + == "Returns the title and author of a book nicely formatted.\n\nReading level: beginner" + ) + assert ( + tool(title="The Name of the Wind", author="Rothfuss, Patrick").call() + == "The Name of the Wind by Rothfuss, Patrick" + ) diff --git a/tests/core/base/test_utils.py b/tests/core/base/test_utils.py index 989450c0..16208fac 100644 --- a/tests/core/base/test_utils.py +++ b/tests/core/base/test_utils.py @@ -32,7 +32,7 @@ def test_format_prompt_template() -> None: "genres": genres, "authors_and_books": authors_and_books, } - formatted_prompt_template = _utils.format_prompt_template(prompt_template, attrs) + formatted_prompt_template = _utils.format_template(prompt_template, attrs) assert ( formatted_prompt_template == dedent( @@ -114,8 +114,7 @@ def fn(model_name: str = "", self=None, cls=None) -> str: def test_convert_function_to_base_model_errors() -> None: """Tests the various `ValueErro` cases in `convert_function_to_base_model`.""" - def empty(param) -> str: - ... # pragma: no cover + def empty(param) -> str: ... # pragma: no cover with pytest.raises(ValueError): _utils.convert_function_to_base_tool(empty, BaseTool) From fc95385215f12f1e5c3e1cd22004f814ca07a756 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Fri, 21 Jun 2024 23:46:56 +0900 Subject: [PATCH 11/32] Improve error message --- mirascope/core/base/toolkit.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mirascope/core/base/toolkit.py b/mirascope/core/base/toolkit.py index 60d9bfc5..93c35219 100644 --- a/mirascope/core/base/toolkit.py +++ b/mirascope/core/base/toolkit.py @@ -16,7 +16,7 @@ def toolkit_tool( - method: Callable[[BaseToolKit, ...], str], + method: Callable[[BaseToolKit, ...], str], ) -> Callable[[BaseToolKit, ...], str]: # Mark the method as a toolkit tool setattr(method, _TOOLKIT_TOOL_METHOD_MARKER, True) @@ -62,7 +62,8 @@ def __pydantic_init_subclass__(cls, **kwargs): if not var.startswith("self."): # Should be supported un-self variables? raise ValueError( - "The toolkit_tool method must use self. prefix in template variables" + "The toolkit_tool method must use self. prefix in template variables " + "when creating tools dynamically" ) self_var = var[5:] From a34fed50b3b59bb1021e8d6c96f11c9f7b928d3a Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Fri, 21 Jun 2024 23:52:39 +0900 Subject: [PATCH 12/32] Fix unittest name --- tests/core/base/test_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/core/base/test_utils.py b/tests/core/base/test_utils.py index 16208fac..de673b39 100644 --- a/tests/core/base/test_utils.py +++ b/tests/core/base/test_utils.py @@ -9,8 +9,8 @@ from mirascope.core.base import _utils, BaseTool -def test_format_prompt_template() -> None: - """Tests the `format_prompt_template` function.""" +def test_format_template() -> None: + """Tests the `format_template` function.""" prompt_template = """ Recommend a {empty_list} book. From 850ed69e97e87b28ec5a19851509a848298a2055 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Fri, 21 Jun 2024 23:52:54 +0900 Subject: [PATCH 13/32] Apply list comprehension --- mirascope/core/base/toolkit.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/mirascope/core/base/toolkit.py b/mirascope/core/base/toolkit.py index 93c35219..de8ae434 100644 --- a/mirascope/core/base/toolkit.py +++ b/mirascope/core/base/toolkit.py @@ -38,12 +38,11 @@ class BaseToolKit(BaseModel, ABC): def create_tools(self) -> list[type[BaseTool]]: """The method to create the tools.""" - tools: list[type[BaseTool]] = [] - for method, template_vars, template in self._toolkit_tool_methods: - formatted_template = template.format(self=self) - tool = convert_function_to_base_tool(method, BaseTool, formatted_template) - tools.append(tool) - return tools + return [ + convert_function_to_base_tool(method, BaseTool, template) + for method, template_vars, template in + self._toolkit_tool_methods + ] @classmethod def __pydantic_init_subclass__(cls, **kwargs): From 6ff6883d140794898f3ee051842d5ba11b735f93 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 22 Jun 2024 00:03:00 +0900 Subject: [PATCH 14/32] change has_self_or_cls to has_self --- mirascope/core/base/_utils.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/mirascope/core/base/_utils.py b/mirascope/core/base/_utils.py index 2157741d..dc3ec21a 100644 --- a/mirascope/core/base/_utils.py +++ b/mirascope/core/base/_utils.py @@ -153,10 +153,12 @@ def convert_function_to_base_tool( field_definitions = {} hints = get_type_hints(fn) - has_self_or_cls = False + has_self = False for i, parameter in enumerate(inspect.signature(fn).parameters.values()): - if parameter.name == "self" or parameter.name == "cls": - has_self_or_cls = True + if parameter.name == "self": + has_self = True + continue + if parameter.name == "cls": continue if parameter.annotation == inspect.Parameter.empty: raise ValueError("All parameters must have a type annotation.") @@ -202,7 +204,7 @@ def convert_function_to_base_tool( def call(self: base): return fn( **( - ({"self": self} if has_self_or_cls else {}) + ({"self": self} if has_self else {}) | { str( self.model_fields[field_name].alias From 35716fd31e9bedc8c0d8426fc18ac59aae83f3da Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 22 Jun 2024 00:39:22 +0900 Subject: [PATCH 15/32] Improve typing --- mirascope/core/base/toolkit.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/mirascope/core/base/toolkit.py b/mirascope/core/base/toolkit.py index de8ae434..d6082daf 100644 --- a/mirascope/core/base/toolkit.py +++ b/mirascope/core/base/toolkit.py @@ -2,10 +2,10 @@ import inspect from abc import ABC -from typing import Callable, ClassVar, ParamSpec, NamedTuple +from typing import Callable, ClassVar, NamedTuple from pydantic import BaseModel, ConfigDict -from typing_extensions import LiteralString +from typing_extensions import LiteralString, ParamSpec, Concatenate from . import BaseTool from ._utils import convert_function_to_base_tool, get_template_variables @@ -16,8 +16,8 @@ def toolkit_tool( - method: Callable[[BaseToolKit, ...], str], -) -> Callable[[BaseToolKit, ...], str]: + method: Callable[Concatenate[BaseToolKit, P], str], +) -> Callable[Concatenate[BaseToolKit, P], str]: # Mark the method as a toolkit tool setattr(method, _TOOLKIT_TOOL_METHOD_MARKER, True) @@ -25,7 +25,7 @@ def toolkit_tool( class TookKitToolMethod(NamedTuple): - method: Callable[[BaseToolKit, ...], str] + method: Callable[..., str] template_vars: list[str] template: str @@ -59,7 +59,6 @@ def __pydantic_init_subclass__(cls, **kwargs): for var in template_vars: if not var.startswith("self."): - # Should be supported un-self variables? raise ValueError( "The toolkit_tool method must use self. prefix in template variables " "when creating tools dynamically" From dbc68763248782390276609e052f7569765cf4fb Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 22 Jun 2024 00:50:02 +0900 Subject: [PATCH 16/32] Fix name --- mirascope/core/base/toolkit.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mirascope/core/base/toolkit.py b/mirascope/core/base/toolkit.py index d6082daf..8cf1a034 100644 --- a/mirascope/core/base/toolkit.py +++ b/mirascope/core/base/toolkit.py @@ -24,7 +24,7 @@ def toolkit_tool( return method -class TookKitToolMethod(NamedTuple): +class ToolKitToolMethod(NamedTuple): method: Callable[..., str] template_vars: list[str] template: str @@ -34,7 +34,7 @@ class BaseToolKit(BaseModel, ABC): """A class for defining tools for LLM call tools.""" model_config = ConfigDict(arbitrary_types_allowed=True) - _toolkit_tool_methods: ClassVar[list[TookKitToolMethod]] + _toolkit_tool_methods: ClassVar[list[ToolKitToolMethod]] def create_tools(self) -> list[type[BaseTool]]: """The method to create the tools.""" @@ -74,7 +74,7 @@ def __pydantic_init_subclass__(cls, **kwargs): ) cls._toolkit_tool_methods.append( - TookKitToolMethod(attr, template_vars, dedented_template) + ToolKitToolMethod(attr, template_vars, dedented_template) ) if not cls._toolkit_tool_methods: raise ValueError("No toolkit_tool method found") From 569f687e986e3b69d99c9dfc338ecd53dee8b90b Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 22 Jun 2024 00:52:59 +0900 Subject: [PATCH 17/32] Small fixes --- mirascope/core/base/toolkit.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mirascope/core/base/toolkit.py b/mirascope/core/base/toolkit.py index 8cf1a034..cf965129 100644 --- a/mirascope/core/base/toolkit.py +++ b/mirascope/core/base/toolkit.py @@ -1,3 +1,4 @@ +"""The module for defining the toolkit class for LLM call tools.""" from __future__ import annotations import inspect @@ -5,12 +6,12 @@ from typing import Callable, ClassVar, NamedTuple from pydantic import BaseModel, ConfigDict -from typing_extensions import LiteralString, ParamSpec, Concatenate +from typing_extensions import ParamSpec, Concatenate from . import BaseTool from ._utils import convert_function_to_base_tool, get_template_variables -_TOOLKIT_TOOL_METHOD_MARKER: LiteralString = "__toolkit_tool_method__" +_TOOLKIT_TOOL_METHOD_MARKER: str = "__toolkit_tool_method__" P = ParamSpec("P") From 85d61f2449f6646ca6037b3e439326584f5fdba9 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 22 Jun 2024 00:55:30 +0900 Subject: [PATCH 18/32] Apply format --- mirascope/core/base/toolkit.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mirascope/core/base/toolkit.py b/mirascope/core/base/toolkit.py index cf965129..324aeb2c 100644 --- a/mirascope/core/base/toolkit.py +++ b/mirascope/core/base/toolkit.py @@ -1,4 +1,5 @@ """The module for defining the toolkit class for LLM call tools.""" + from __future__ import annotations import inspect @@ -17,7 +18,7 @@ def toolkit_tool( - method: Callable[Concatenate[BaseToolKit, P], str], + method: Callable[Concatenate[BaseToolKit, P], str], ) -> Callable[Concatenate[BaseToolKit, P], str]: # Mark the method as a toolkit tool setattr(method, _TOOLKIT_TOOL_METHOD_MARKER, True) @@ -41,8 +42,7 @@ def create_tools(self) -> list[type[BaseTool]]: """The method to create the tools.""" return [ convert_function_to_base_tool(method, BaseTool, template) - for method, template_vars, template in - self._toolkit_tool_methods + for method, template_vars, template in self._toolkit_tool_methods ] @classmethod From f2139439e9230fcb6acdf68c66ca3a611dd48f32 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 22 Jun 2024 01:09:28 +0900 Subject: [PATCH 19/32] Add namespace --- mirascope/core/base/_utils.py | 5 +++-- mirascope/core/base/toolkit.py | 5 ++++- tests/core/base/test_toolkit.py | 13 +++++++------ 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/mirascope/core/base/_utils.py b/mirascope/core/base/_utils.py index dc3ec21a..7914bbcc 100644 --- a/mirascope/core/base/_utils.py +++ b/mirascope/core/base/_utils.py @@ -123,7 +123,7 @@ def parse_prompt_messages( def convert_function_to_base_tool( - fn: Callable, base: type[BaseToolT], __doc__: str | None = None + fn: Callable, base: type[BaseToolT], __doc__: str | None = None, namespace: str | None = None ) -> type[BaseToolT]: """Constructst a `BaseToolT` type from the given function. @@ -135,6 +135,7 @@ def convert_function_to_base_tool( fn: The function to convert. base: The `BaseToolT` type to which the function is converted. __doc__: The docstring to use for the constructed `BaseToolT` type. + namespace: The namespace to use for the constructed `BaseToolT` type. Returns: The constructed `BaseToolT` type. @@ -195,7 +196,7 @@ def convert_function_to_base_tool( ) model = create_model( - fn.__name__, + f"{namespace}.{fn.__name__}" if namespace else fn.__name__, __base__=base, __doc__=inspect.cleandoc(func_doc) if func_doc else DEFAULT_TOOL_DOCSTRING, **cast(dict[str, Any], field_definitions), diff --git a/mirascope/core/base/toolkit.py b/mirascope/core/base/toolkit.py index 324aeb2c..61fadd3c 100644 --- a/mirascope/core/base/toolkit.py +++ b/mirascope/core/base/toolkit.py @@ -37,11 +37,14 @@ class BaseToolKit(BaseModel, ABC): model_config = ConfigDict(arbitrary_types_allowed=True) _toolkit_tool_methods: ClassVar[list[ToolKitToolMethod]] + _namespace: ClassVar[str | None] = None def create_tools(self) -> list[type[BaseTool]]: """The method to create the tools.""" return [ - convert_function_to_base_tool(method, BaseTool, template) + convert_function_to_base_tool( + method, BaseTool, template.format(self=self), self._namespace + ) for method, template_vars, template in self._toolkit_tool_methods ] diff --git a/tests/core/base/test_toolkit.py b/tests/core/base/test_toolkit.py index a7c768e9..fba9384f 100644 --- a/tests/core/base/test_toolkit.py +++ b/tests/core/base/test_toolkit.py @@ -1,5 +1,5 @@ """Tests for the `toolkit` module.""" -from typing import Literal +from typing import Literal, ClassVar from mirascope.core.base import BaseToolKit, toolkit_tool @@ -10,6 +10,7 @@ def test_toolkit() -> None: class BookRecommendationToolKit(BaseToolKit): """A toolkit for recommending books.""" + _namespace: ClassVar[str] = 'book_tools' reading_level: Literal["beginner", "advanced"] @toolkit_tool @@ -24,12 +25,12 @@ def format_book(self, title: str, author: str) -> str: tools = toolkit.create_tools() assert len(tools) == 1 tool = tools[0] - assert tool._name() == "format_book" + assert tool._name() == "book_tools.format_book" assert ( - tool._description() - == "Returns the title and author of a book nicely formatted.\n\nReading level: beginner" + tool._description() + == "Returns the title and author of a book nicely formatted.\n\nReading level: beginner" ) assert ( - tool(title="The Name of the Wind", author="Rothfuss, Patrick").call() - == "The Name of the Wind by Rothfuss, Patrick" + tool(title="The Name of the Wind", author="Rothfuss, Patrick").call() + == "The Name of the Wind by Rothfuss, Patrick" ) From c5a3eb3f8acb66e4ff5007243f6d5ae2edc3ad98 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 22 Jun 2024 01:17:03 +0900 Subject: [PATCH 20/32] Add testcase --- tests/core/base/test_toolkit.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/core/base/test_toolkit.py b/tests/core/base/test_toolkit.py index fba9384f..34833416 100644 --- a/tests/core/base/test_toolkit.py +++ b/tests/core/base/test_toolkit.py @@ -1,16 +1,25 @@ """Tests for the `toolkit` module.""" from typing import Literal, ClassVar +import pytest + from mirascope.core.base import BaseToolKit, toolkit_tool -def test_toolkit() -> None: +@pytest.mark.parametrize( + "namespace, expected_name", + [ + (None, "format_book"), + ("book_tools", "book_tools.format_book"), + ], +) +def test_toolkit(namespace: str | None, expected_name: str) -> None: """Tests the `BaseToolKit` class and the `toolkit_tool` decorator.""" class BookRecommendationToolKit(BaseToolKit): """A toolkit for recommending books.""" - _namespace: ClassVar[str] = 'book_tools' + _namespace: ClassVar[str] = namespace reading_level: Literal["beginner", "advanced"] @toolkit_tool @@ -25,7 +34,7 @@ def format_book(self, title: str, author: str) -> str: tools = toolkit.create_tools() assert len(tools) == 1 tool = tools[0] - assert tool._name() == "book_tools.format_book" + assert tool._name() == expected_name assert ( tool._description() == "Returns the title and author of a book nicely formatted.\n\nReading level: beginner" From 1c9d3d8ba9cda047c315b42999beafbb04c23430 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 22 Jun 2024 01:37:42 +0900 Subject: [PATCH 21/32] Add testcase --- tests/core/base/test_toolkit.py | 128 ++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/tests/core/base/test_toolkit.py b/tests/core/base/test_toolkit.py index 34833416..54d232b7 100644 --- a/tests/core/base/test_toolkit.py +++ b/tests/core/base/test_toolkit.py @@ -43,3 +43,131 @@ def format_book(self, title: str, author: str) -> str: tool(title="The Name of the Wind", author="Rothfuss, Patrick").call() == "The Name of the Wind by Rothfuss, Patrick" ) + + +def test_toolkit_multiple_method() -> None: + """Toolkits with multiple toolkit_tool methods should be created correctly.""" + + def dummy_decorator(func): + return func + + class BookRecommendationToolKit(BaseToolKit): + """A toolkit for recommending books.""" + + _namespace: ClassVar[str] = 'book_tools' + reading_level: Literal["beginner", "advanced"] + language: Literal["english", "spanish", "french"] + + @toolkit_tool + def format_book(self, title: str, author: str) -> str: + """Returns the title and author of a book nicely formatted. + + Reading level: {self.reading_level} + """ + return f"{title} by {author}" + + @toolkit_tool + def format_world_book(self, title: str, author: str, genre: str) -> str: + """Returns the title, author, and genre of a book nicely formatted. + + Reading level: {self.reading_level} + language: {self.language} + """ + return f"{title} by {author} ({genre})" + + @dummy_decorator + def dummy_method(self): + """dummy method""" + return "dummy" + + toolkit = BookRecommendationToolKit(reading_level="beginner", language="spanish") + tools = toolkit.create_tools() + assert len(tools) == 2 + + assert tools[0]._name() == 'book_tools.format_book' + assert ( + tools[0]._description() + == "Returns the title and author of a book nicely formatted.\n\nReading level: beginner" + ) + assert ( + tools[0](title="The Name of the Wind", author="Rothfuss, Patrick").call() + == "The Name of the Wind by Rothfuss, Patrick" + ) + assert tools[1]._name() == 'book_tools.format_world_book' + assert ( + tools[1]._description() + == "Returns the title, author, and genre of a book nicely formatted.\n\nReading level: beginner\nlanguage: spanish" + ) + assert ( + tools[1](title="The Name of the Wind", author="Rothfuss, Patrick", genre="fantasy").call() + == "The Name of the Wind by Rothfuss, Patrick (fantasy)" + ) + + +def test_toolkit_tool_method_not_found() -> None: + """When a toolkit_tool method is not found, a ValueError should be raised.""" + + def dummy_decorator(func): + return func + + with pytest.raises(ValueError, match="No toolkit_tool method found"): + class BookRecommendationToolKit(BaseToolKit): + """A toolkit for recommending books.""" + + _namespace: ClassVar[str] = 'book_tools' + reading_level: Literal["beginner", "advanced"] + language: Literal["english", "spanish", "french"] + + def format_book(self, title: str, author: str) -> str: + """Returns the title and author of a book nicely formatted. + + Reading level: {self.reading_level} + """ + return f"{title} by {author}" + + @dummy_decorator + def dummy_method(self): + """dummy method""" + return "dummy" + + +def test_toolkit_tool_method_has_non_self_var() -> None: + """check if toolkit_tool method has non-self variable, a ValueError should be raised.""" + + with pytest.raises(ValueError, + match="The toolkit_tool method must use self. prefix in template variables when creating tools dynamically"): + class BookRecommendationToolKit(BaseToolKit): + """A toolkit for recommending books.""" + + _namespace: ClassVar[str] = 'book_tools' + reading_level: Literal["beginner", "advanced"] + language: Literal["english", "spanish", "french"] + + @toolkit_tool + def format_book(self, title: str, author: str) -> str: + """Returns the title and author of a book nicely formatted. + + Reading level: {reading_level} + """ + return f"{title} by {author}" + + +def test_toolkit_tool_method_has_no_exists_var() -> None: + """check if toolkit_tool method has no exists variable, a ValueError should be raised.""" + + with pytest.raises(ValueError, + match="The toolkit_tool method template variable self.not_exists is not found in the class"): + class BookRecommendationToolKit(BaseToolKit): + """A toolkit for recommending books.""" + + _namespace: ClassVar[str] = 'book_tools' + reading_level: Literal["beginner", "advanced"] + language: Literal["english", "spanish", "french"] + + @toolkit_tool + def format_book(self, title: str, author: str) -> str: + """Returns the title and author of a book nicely formatted. + + Reading level: {self.not_exists} + """ + return f"{title} by {author}" From 9892a2b1b1eee08e2c08b67a91a88b89a46d0138 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 22 Jun 2024 01:58:51 +0900 Subject: [PATCH 22/32] Add namespace checking --- mirascope/core/base/toolkit.py | 12 ++++++++++-- tests/core/base/test_toolkit.py | 12 ++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/mirascope/core/base/toolkit.py b/mirascope/core/base/toolkit.py index 61fadd3c..7e47e86a 100644 --- a/mirascope/core/base/toolkit.py +++ b/mirascope/core/base/toolkit.py @@ -14,11 +14,13 @@ _TOOLKIT_TOOL_METHOD_MARKER: str = "__toolkit_tool_method__" +_namespaces: set[str] = set() + P = ParamSpec("P") def toolkit_tool( - method: Callable[Concatenate[BaseToolKit, P], str], + method: Callable[Concatenate[BaseToolKit, P], str], ) -> Callable[Concatenate[BaseToolKit, P], str]: # Mark the method as a toolkit tool setattr(method, _TOOLKIT_TOOL_METHOD_MARKER, True) @@ -37,7 +39,7 @@ class BaseToolKit(BaseModel, ABC): model_config = ConfigDict(arbitrary_types_allowed=True) _toolkit_tool_methods: ClassVar[list[ToolKitToolMethod]] - _namespace: ClassVar[str | None] = None + _namespaces: ClassVar[str | None] = None def create_tools(self) -> list[type[BaseTool]]: """The method to create the tools.""" @@ -50,6 +52,12 @@ def create_tools(self) -> list[type[BaseTool]]: @classmethod def __pydantic_init_subclass__(cls, **kwargs): + # validate the namespace + if cls._namespace is not None: + if cls._namespace in _namespaces: + raise ValueError(f"The namespace {cls._namespace} is already used") + _namespaces.add(cls._namespace) + cls._toolkit_tool_methods = [] for attr in cls.__dict__.values(): if not getattr(attr, _TOOLKIT_TOOL_METHOD_MARKER, False): diff --git a/tests/core/base/test_toolkit.py b/tests/core/base/test_toolkit.py index 54d232b7..f8854f80 100644 --- a/tests/core/base/test_toolkit.py +++ b/tests/core/base/test_toolkit.py @@ -1,5 +1,6 @@ """Tests for the `toolkit` module.""" from typing import Literal, ClassVar +from unittest import mock import pytest @@ -171,3 +172,14 @@ def format_book(self, title: str, author: str) -> str: Reading level: {self.not_exists} """ return f"{title} by {author}" + + +def test_toolkit_namespace_already_used() -> None: + """check if toolkit_tool namespace is already used, a ValueError should be raised.""" + with (mock.patch('mirascope.core.base.toolkit._namespaces', {'book_tools'}), + pytest.raises(ValueError, + match="The namespace book_tools is already used")): + class BookRecommendationToolKit(BaseToolKit): + """A toolkit for recommending books.""" + + _namespace: ClassVar[str] = 'book_tools' From 4bf7b2c7371236beafab81b24c234ec47ff735f4 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 22 Jun 2024 02:00:26 +0900 Subject: [PATCH 23/32] Fix namespace --- mirascope/core/base/toolkit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mirascope/core/base/toolkit.py b/mirascope/core/base/toolkit.py index 7e47e86a..8f7aee14 100644 --- a/mirascope/core/base/toolkit.py +++ b/mirascope/core/base/toolkit.py @@ -39,7 +39,7 @@ class BaseToolKit(BaseModel, ABC): model_config = ConfigDict(arbitrary_types_allowed=True) _toolkit_tool_methods: ClassVar[list[ToolKitToolMethod]] - _namespaces: ClassVar[str | None] = None + _namespace: ClassVar[str | None] = None def create_tools(self) -> list[type[BaseTool]]: """The method to create the tools.""" From fe6f4b39be8efcd212764d4f9fa1d6346787fdb2 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 22 Jun 2024 02:06:36 +0900 Subject: [PATCH 24/32] Fix namespace --- mirascope/core/base/toolkit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mirascope/core/base/toolkit.py b/mirascope/core/base/toolkit.py index 8f7aee14..aae3b09b 100644 --- a/mirascope/core/base/toolkit.py +++ b/mirascope/core/base/toolkit.py @@ -20,7 +20,7 @@ def toolkit_tool( - method: Callable[Concatenate[BaseToolKit, P], str], + method: Callable[Concatenate[BaseToolKit, P], str], ) -> Callable[Concatenate[BaseToolKit, P], str]: # Mark the method as a toolkit tool setattr(method, _TOOLKIT_TOOL_METHOD_MARKER, True) @@ -53,7 +53,7 @@ def create_tools(self) -> list[type[BaseTool]]: @classmethod def __pydantic_init_subclass__(cls, **kwargs): # validate the namespace - if cls._namespace is not None: + if cls._namespace: if cls._namespace in _namespaces: raise ValueError(f"The namespace {cls._namespace} is already used") _namespaces.add(cls._namespace) From 62e6845aa16ced968eb3da86900aeb9b501695ec Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 22 Jun 2024 02:10:19 +0900 Subject: [PATCH 25/32] Format unittest --- tests/core/base/test_toolkit.py | 74 ++++++++++++++++++++------------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/tests/core/base/test_toolkit.py b/tests/core/base/test_toolkit.py index f8854f80..845be1b0 100644 --- a/tests/core/base/test_toolkit.py +++ b/tests/core/base/test_toolkit.py @@ -1,4 +1,5 @@ """Tests for the `toolkit` module.""" + from typing import Literal, ClassVar from unittest import mock @@ -20,7 +21,7 @@ def test_toolkit(namespace: str | None, expected_name: str) -> None: class BookRecommendationToolKit(BaseToolKit): """A toolkit for recommending books.""" - _namespace: ClassVar[str] = namespace + _namespace: ClassVar[str | None] = namespace reading_level: Literal["beginner", "advanced"] @toolkit_tool @@ -37,12 +38,12 @@ def format_book(self, title: str, author: str) -> str: tool = tools[0] assert tool._name() == expected_name assert ( - tool._description() - == "Returns the title and author of a book nicely formatted.\n\nReading level: beginner" + tool._description() + == "Returns the title and author of a book nicely formatted.\n\nReading level: beginner" ) assert ( - tool(title="The Name of the Wind", author="Rothfuss, Patrick").call() - == "The Name of the Wind by Rothfuss, Patrick" + tool(title="The Name of the Wind", author="Rothfuss, Patrick").call() + == "The Name of the Wind by Rothfuss, Patrick" ) @@ -55,7 +56,7 @@ def dummy_decorator(func): class BookRecommendationToolKit(BaseToolKit): """A toolkit for recommending books.""" - _namespace: ClassVar[str] = 'book_tools' + _namespace: ClassVar[str | None] = "book_tools" reading_level: Literal["beginner", "advanced"] language: Literal["english", "spanish", "french"] @@ -85,23 +86,26 @@ def dummy_method(self): tools = toolkit.create_tools() assert len(tools) == 2 - assert tools[0]._name() == 'book_tools.format_book' + assert tools[0]._name() == "book_tools.format_book" assert ( - tools[0]._description() - == "Returns the title and author of a book nicely formatted.\n\nReading level: beginner" + tools[0]._description() + == "Returns the title and author of a book nicely formatted.\n\nReading level: beginner" ) assert ( - tools[0](title="The Name of the Wind", author="Rothfuss, Patrick").call() - == "The Name of the Wind by Rothfuss, Patrick" + tools[0](title="The Name of the Wind", author="Rothfuss, Patrick").call() + == "The Name of the Wind by Rothfuss, Patrick" ) - assert tools[1]._name() == 'book_tools.format_world_book' + assert tools[1]._name() == "book_tools.format_world_book" assert ( - tools[1]._description() - == "Returns the title, author, and genre of a book nicely formatted.\n\nReading level: beginner\nlanguage: spanish" + tools[1]._description() + == "Returns the title, author, and genre of a book nicely formatted.\n\nReading level: beginner\n" + "language: spanish" ) assert ( - tools[1](title="The Name of the Wind", author="Rothfuss, Patrick", genre="fantasy").call() - == "The Name of the Wind by Rothfuss, Patrick (fantasy)" + tools[1]( + title="The Name of the Wind", author="Rothfuss, Patrick", genre="fantasy" + ).call() + == "The Name of the Wind by Rothfuss, Patrick (fantasy)" ) @@ -112,10 +116,11 @@ def dummy_decorator(func): return func with pytest.raises(ValueError, match="No toolkit_tool method found"): + class BookRecommendationToolKit(BaseToolKit): """A toolkit for recommending books.""" - _namespace: ClassVar[str] = 'book_tools' + _namespace: ClassVar[str | None] = "book_tools" reading_level: Literal["beginner", "advanced"] language: Literal["english", "spanish", "french"] @@ -133,14 +138,18 @@ def dummy_method(self): def test_toolkit_tool_method_has_non_self_var() -> None: - """check if toolkit_tool method has non-self variable, a ValueError should be raised.""" + """Check if toolkit_tool method has non-self variable, a ValueError should be raised.""" + + with pytest.raises( + ValueError, + match="The toolkit_tool method must use self. prefix in template variables " + "when creating tools dynamically", + ): - with pytest.raises(ValueError, - match="The toolkit_tool method must use self. prefix in template variables when creating tools dynamically"): class BookRecommendationToolKit(BaseToolKit): """A toolkit for recommending books.""" - _namespace: ClassVar[str] = 'book_tools' + _namespace: ClassVar[str | None] = "book_tools" reading_level: Literal["beginner", "advanced"] language: Literal["english", "spanish", "french"] @@ -154,14 +163,17 @@ def format_book(self, title: str, author: str) -> str: def test_toolkit_tool_method_has_no_exists_var() -> None: - """check if toolkit_tool method has no exists variable, a ValueError should be raised.""" + """Check if toolkit_tool method has no exists variable, a ValueError should be raised.""" + + with pytest.raises( + ValueError, + match="The toolkit_tool method template variable self.not_exists is not found in the class", + ): - with pytest.raises(ValueError, - match="The toolkit_tool method template variable self.not_exists is not found in the class"): class BookRecommendationToolKit(BaseToolKit): """A toolkit for recommending books.""" - _namespace: ClassVar[str] = 'book_tools' + _namespace: ClassVar[str | None] = "book_tools" reading_level: Literal["beginner", "advanced"] language: Literal["english", "spanish", "french"] @@ -175,11 +187,13 @@ def format_book(self, title: str, author: str) -> str: def test_toolkit_namespace_already_used() -> None: - """check if toolkit_tool namespace is already used, a ValueError should be raised.""" - with (mock.patch('mirascope.core.base.toolkit._namespaces', {'book_tools'}), - pytest.raises(ValueError, - match="The namespace book_tools is already used")): + """Check if toolkit_tool namespace is already used, a ValueError should be raised.""" + with ( + mock.patch("mirascope.core.base.toolkit._namespaces", {"book_tools"}), + pytest.raises(ValueError, match="The namespace book_tools is already used"), + ): + class BookRecommendationToolKit(BaseToolKit): """A toolkit for recommending books.""" - _namespace: ClassVar[str] = 'book_tools' + _namespace: ClassVar[str | None] = "book_tools" From 275d397004a1400d2c012c3b8af12f4a1914a2ae Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 22 Jun 2024 02:20:08 +0900 Subject: [PATCH 26/32] Fix unittest --- tests/core/base/test_toolkit.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/tests/core/base/test_toolkit.py b/tests/core/base/test_toolkit.py index 845be1b0..18a5fbe0 100644 --- a/tests/core/base/test_toolkit.py +++ b/tests/core/base/test_toolkit.py @@ -8,6 +8,13 @@ from mirascope.core.base import BaseToolKit, toolkit_tool +@pytest.fixture +def mock_namespaces(): + mock_namespaces = set() + with mock.patch("mirascope.core.base.toolkit._namespaces", mock_namespaces): + yield mock_namespaces + + @pytest.mark.parametrize( "namespace, expected_name", [ @@ -15,7 +22,7 @@ ("book_tools", "book_tools.format_book"), ], ) -def test_toolkit(namespace: str | None, expected_name: str) -> None: +def test_toolkit(mock_namespaces, namespace: str | None, expected_name: str) -> None: """Tests the `BaseToolKit` class and the `toolkit_tool` decorator.""" class BookRecommendationToolKit(BaseToolKit): @@ -47,7 +54,7 @@ def format_book(self, title: str, author: str) -> str: ) -def test_toolkit_multiple_method() -> None: +def test_toolkit_multiple_method(mock_namespaces) -> None: """Toolkits with multiple toolkit_tool methods should be created correctly.""" def dummy_decorator(func): @@ -137,7 +144,7 @@ def dummy_method(self): return "dummy" -def test_toolkit_tool_method_has_non_self_var() -> None: +def test_toolkit_tool_method_has_non_self_var(mock_namespaces) -> None: """Check if toolkit_tool method has non-self variable, a ValueError should be raised.""" with pytest.raises( @@ -162,7 +169,7 @@ def format_book(self, title: str, author: str) -> str: return f"{title} by {author}" -def test_toolkit_tool_method_has_no_exists_var() -> None: +def test_toolkit_tool_method_has_no_exists_var(mock_namespaces) -> None: """Check if toolkit_tool method has no exists variable, a ValueError should be raised.""" with pytest.raises( @@ -186,12 +193,11 @@ def format_book(self, title: str, author: str) -> str: return f"{title} by {author}" -def test_toolkit_namespace_already_used() -> None: +def test_toolkit_namespace_already_used(mock_namespaces) -> None: """Check if toolkit_tool namespace is already used, a ValueError should be raised.""" - with ( - mock.patch("mirascope.core.base.toolkit._namespaces", {"book_tools"}), - pytest.raises(ValueError, match="The namespace book_tools is already used"), - ): + + mock_namespaces.add("book_tools") + with pytest.raises(ValueError, match="The namespace book_tools is already used"): class BookRecommendationToolKit(BaseToolKit): """A toolkit for recommending books.""" From 51420b8761c7398596e8a17bd05ffe92954dc742 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 22 Jun 2024 02:23:14 +0900 Subject: [PATCH 27/32] Change _namespace to __namespace__ --- mirascope/core/base/toolkit.py | 12 ++++++------ tests/core/base/test_toolkit.py | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/mirascope/core/base/toolkit.py b/mirascope/core/base/toolkit.py index aae3b09b..aabae74c 100644 --- a/mirascope/core/base/toolkit.py +++ b/mirascope/core/base/toolkit.py @@ -39,13 +39,13 @@ class BaseToolKit(BaseModel, ABC): model_config = ConfigDict(arbitrary_types_allowed=True) _toolkit_tool_methods: ClassVar[list[ToolKitToolMethod]] - _namespace: ClassVar[str | None] = None + __namespace__: ClassVar[str | None] = None def create_tools(self) -> list[type[BaseTool]]: """The method to create the tools.""" return [ convert_function_to_base_tool( - method, BaseTool, template.format(self=self), self._namespace + method, BaseTool, template.format(self=self), self.__namespace__ ) for method, template_vars, template in self._toolkit_tool_methods ] @@ -53,10 +53,10 @@ def create_tools(self) -> list[type[BaseTool]]: @classmethod def __pydantic_init_subclass__(cls, **kwargs): # validate the namespace - if cls._namespace: - if cls._namespace in _namespaces: - raise ValueError(f"The namespace {cls._namespace} is already used") - _namespaces.add(cls._namespace) + if cls.__namespace__: + if cls.__namespace__ in _namespaces: + raise ValueError(f"The namespace {cls.__namespace__} is already used") + _namespaces.add(cls.__namespace__) cls._toolkit_tool_methods = [] for attr in cls.__dict__.values(): diff --git a/tests/core/base/test_toolkit.py b/tests/core/base/test_toolkit.py index 18a5fbe0..f401333b 100644 --- a/tests/core/base/test_toolkit.py +++ b/tests/core/base/test_toolkit.py @@ -28,7 +28,7 @@ def test_toolkit(mock_namespaces, namespace: str | None, expected_name: str) -> class BookRecommendationToolKit(BaseToolKit): """A toolkit for recommending books.""" - _namespace: ClassVar[str | None] = namespace + __namespace__: ClassVar[str | None] = namespace reading_level: Literal["beginner", "advanced"] @toolkit_tool @@ -63,7 +63,7 @@ def dummy_decorator(func): class BookRecommendationToolKit(BaseToolKit): """A toolkit for recommending books.""" - _namespace: ClassVar[str | None] = "book_tools" + __namespace__: ClassVar[str | None] = "book_tools" reading_level: Literal["beginner", "advanced"] language: Literal["english", "spanish", "french"] @@ -127,7 +127,7 @@ def dummy_decorator(func): class BookRecommendationToolKit(BaseToolKit): """A toolkit for recommending books.""" - _namespace: ClassVar[str | None] = "book_tools" + __namespace__: ClassVar[str | None] = "book_tools" reading_level: Literal["beginner", "advanced"] language: Literal["english", "spanish", "french"] @@ -156,7 +156,7 @@ def test_toolkit_tool_method_has_non_self_var(mock_namespaces) -> None: class BookRecommendationToolKit(BaseToolKit): """A toolkit for recommending books.""" - _namespace: ClassVar[str | None] = "book_tools" + __namespace__: ClassVar[str | None] = "book_tools" reading_level: Literal["beginner", "advanced"] language: Literal["english", "spanish", "french"] @@ -180,7 +180,7 @@ def test_toolkit_tool_method_has_no_exists_var(mock_namespaces) -> None: class BookRecommendationToolKit(BaseToolKit): """A toolkit for recommending books.""" - _namespace: ClassVar[str | None] = "book_tools" + __namespace__: ClassVar[str | None] = "book_tools" reading_level: Literal["beginner", "advanced"] language: Literal["english", "spanish", "french"] @@ -202,4 +202,4 @@ def test_toolkit_namespace_already_used(mock_namespaces) -> None: class BookRecommendationToolKit(BaseToolKit): """A toolkit for recommending books.""" - _namespace: ClassVar[str | None] = "book_tools" + __namespace__: ClassVar[str | None] = "book_tools" From 7cf391e303cfff0440a9df913c2139b7f0edb758 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 22 Jun 2024 02:25:36 +0900 Subject: [PATCH 28/32] Format --- mirascope/core/base/_utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mirascope/core/base/_utils.py b/mirascope/core/base/_utils.py index 103a42ed..38115862 100644 --- a/mirascope/core/base/_utils.py +++ b/mirascope/core/base/_utils.py @@ -125,7 +125,10 @@ def parse_prompt_messages( def convert_function_to_base_tool( - fn: Callable, base: type[BaseToolT], __doc__: str | None = None, namespace: str | None = None + fn: Callable, + base: type[BaseToolT], + __doc__: str | None = None, + namespace: str | None = None, ) -> type[BaseToolT]: """Constructst a `BaseToolT` type from the given function. From 1445caa3c375f51a4e72b9cd0d09bb82d89da871 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 22 Jun 2024 02:28:00 +0900 Subject: [PATCH 29/32] Apply format --- examples/logging/logging_to_csv.py | 1 + examples/tool_calls/tool_calls_with_examples.py | 1 + 2 files changed, 2 insertions(+) diff --git a/examples/logging/logging_to_csv.py b/examples/logging/logging_to_csv.py index df192c9e..bd5ae9fe 100644 --- a/examples/logging/logging_to_csv.py +++ b/examples/logging/logging_to_csv.py @@ -1,4 +1,5 @@ """Logging your LLM responses to a CSV file""" + import os import pandas as pd diff --git a/examples/tool_calls/tool_calls_with_examples.py b/examples/tool_calls/tool_calls_with_examples.py index 721572e7..66b52ceb 100644 --- a/examples/tool_calls/tool_calls_with_examples.py +++ b/examples/tool_calls/tool_calls_with_examples.py @@ -2,6 +2,7 @@ You can add examples to your tool definitions to help the model better use the tool. Examples can be added for individual fields as well as for the entire model. """ + import os from pydantic import ConfigDict, Field From 469315a66d88be8ddf67ba2c9c7ae21ab922ac85 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 22 Jun 2024 02:31:16 +0900 Subject: [PATCH 30/32] Add docstring --- mirascope/core/base/_utils.py | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/mirascope/core/base/_utils.py b/mirascope/core/base/_utils.py index 38115862..dd5efce0 100644 --- a/mirascope/core/base/_utils.py +++ b/mirascope/core/base/_utils.py @@ -34,14 +34,29 @@ def get_template_variables(template: str) -> list[str]: - """Returns the variables in the given template string.""" + """Returns the variables in the given template string. + + Args: + template: The template string to parse. + + Returns: + The variables in the template string. + """ return [var for _, var, _, _ in Formatter().parse(template) if var is not None] def get_template_values( template_variables: list[str], attrs: dict[str, Any] ) -> dict[str, Any]: - """Returns the values of the given `template_variables` from the provided `attrs`.""" + """Returns the values of the given `template_variables` from the provided `attrs`. + + Args: + template_variables: The variables to extract from the `attrs`. + attrs: The attributes to extract the variables from. + + Returns: + The values of the template variables. + """ values = {} if "self" in attrs: values["self"] = attrs.get("self") @@ -64,7 +79,16 @@ def get_template_values( def format_template(template: str, attrs: dict[str, Any]) -> str: - """Formats the given prompt `template`""" + """Formats the given prompt `template` + + Args: + template: The template to format. + attrs: The attributes to use for formatting. + + Returns: + The formatted template. + + """ dedented_template = inspect.cleandoc(template).strip() template_vars = get_template_variables(dedented_template) From 95e7bd49ae083897c367e790ede2ddabb1c8f565 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 22 Jun 2024 02:32:41 +0900 Subject: [PATCH 31/32] Fix __namespace__ argument --- mirascope/core/base/_utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mirascope/core/base/_utils.py b/mirascope/core/base/_utils.py index dd5efce0..cbeb222e 100644 --- a/mirascope/core/base/_utils.py +++ b/mirascope/core/base/_utils.py @@ -152,7 +152,7 @@ def convert_function_to_base_tool( fn: Callable, base: type[BaseToolT], __doc__: str | None = None, - namespace: str | None = None, + __namespace__: str | None = None, ) -> type[BaseToolT]: """Constructst a `BaseToolT` type from the given function. @@ -164,7 +164,7 @@ def convert_function_to_base_tool( fn: The function to convert. base: The `BaseToolT` type to which the function is converted. __doc__: The docstring to use for the constructed `BaseToolT` type. - namespace: The namespace to use for the constructed `BaseToolT` type. + __namespace__: The namespace to use for the constructed `BaseToolT` type. Returns: The constructed `BaseToolT` type. @@ -225,7 +225,7 @@ def convert_function_to_base_tool( ) model = create_model( - f"{namespace}.{fn.__name__}" if namespace else fn.__name__, + f"{__namespace__}.{fn.__name__}" if __namespace__ else fn.__name__, __base__=base, __doc__=inspect.cleandoc(func_doc) if func_doc else DEFAULT_TOOL_DOCSTRING, **cast(dict[str, Any], field_definitions), From dfa318e40a3c532931f3aa8ab5bd2cdb34fe7047 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 22 Jun 2024 02:52:18 +0900 Subject: [PATCH 32/32] Improve docstring --- mirascope/core/base/toolkit.py | 43 +++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/mirascope/core/base/toolkit.py b/mirascope/core/base/toolkit.py index aabae74c..f58b1ef5 100644 --- a/mirascope/core/base/toolkit.py +++ b/mirascope/core/base/toolkit.py @@ -35,7 +35,48 @@ class ToolKitToolMethod(NamedTuple): class BaseToolKit(BaseModel, ABC): - """A class for defining tools for LLM call tools.""" + """A class for defining tools for LLM call tools. + + The class should have methods decorated with `@toolkit_tool` to create tools. + + Example: + ```python + from mirascope.core.base import BaseToolKit, toolkit_tool + from mirascope.core.openai import openai_call + + class BookRecommendationToolKit(BaseToolKit): + '''A toolkit for recommending books.''' + + __namespace__: ClassVar[str | None] = 'book_tools' + reading_level: Literal["beginner", "advanced"] + + @toolkit_tool + def format_book(self, title: str, author: str) -> str: + '''Returns the title and author of a book nicely formatted. + + Reading level: {self.reading_level} + ''' + return f"{title} by {author}" + + toolkit = BookRecommendationToolKit(reading_level="beginner") + tools = toolkit.create_tools() + + @openai_call(model="gpt-4o") + def recommend_book(genre: str, reading_level: Literal["beginner", "advanced"]): + '''Recommend a {genre} book.''' + toolkit = BookRecommendationToolKit(reading_level=reading_level) + return {"tools": [toolkit.create_tools()]} + + response = recommend_book("fantasy", "beginner") + if tool := response.tool: + output = tool.call() + print(output) + #> The Name of the Wind by Patrick Rothfuss + else: + print(response.content) + #> Sure! I would recommend... + ``` + """ model_config = ConfigDict(arbitrary_types_allowed=True) _toolkit_tool_methods: ClassVar[list[ToolKitToolMethod]]