Source code for pharia_skill.llama3.tool
import json
import re
from typing import Any, Literal
from pydantic import BaseModel
[docs]
class Tool(BaseModel):
"""Provide a tool definition as a Pydantic model.
The name of the class will be used as function name. The description of the
function is taken from the docstring of the class. The parameters are
specified as attributes of the model. Type hints and default arguments can
be used to specify the schema, and a description of a parameter can be added
with the `Field` class.
Example::
from pydantic import BaseMode, Field
class GetImageInformation(BaseModel):
"Retrieve information about a specific image."
registry: str
repository: str = Field(
description="The full identifier of the image in the registry",
)
tag: str = "latest"
"""
[docs]
@classmethod
def json_schema(cls) -> dict[str, Any]:
"""The (slightly incompliant) json schema of a tool.
This schema is used in two ways:
1. Passed to the model to define the tool
2. For serialization to json as part of a chat request
For all specified tools, this schema is passed to the model.
LLama expects a json object with `type` "function" as the root elements
and a `function` object with the keys `name`, `description`, and `parameters`.
Only for the parameters, we can make use of the json schema representation of a
pydantic models. Note that the output schema is invalid json schema, as there is
no `function` type in the json schema specification:
https://json-schema.org/draft/2020-12/json-schema-validation#section-6.1.1
"""
schema = cls.model_json_schema()
description = schema.get("description")
if description is not None:
del schema["description"]
data = {
"type": "function",
"function": {
"name": cls.name(),
"description": description,
"parameters": schema,
},
}
cls._recursive_purge_title(data)
return data
@classmethod
def _to_snake_case(cls, name: str) -> str:
return re.sub(r"(?<!^)(?=[A-Z])", "_", name).lower()
@classmethod
def _recursive_purge_title(cls, data: dict[str, Any]) -> None:
"""Remove the title field from a dictionary recursively.
The title is automatically created based on the name of the pydantic model,
but it is not shown in examples of the llama model card, hence we skip it.
See https://github.com/pydantic/pydantic/discussions/8504 for more detail.
"""
if isinstance(data, dict):
for key in list(data.keys()):
if key == "title" and "type" in data.keys():
del data[key]
else:
cls._recursive_purge_title(data[key])
[docs]
def render(self) -> str:
"""Convert a tool call to prompt format again.
When a tool call has been loaded from a model response, it is part of the
conversation and needs to be converted back to a prompt when providing the
full conversation history to the model for the next turn.
"""
return json.dumps(
{
"name": self.name(),
"parameters": self.model_dump(exclude_unset=True),
}
)
class Function(BaseModel):
name: str
parameters: dict[str, Any]
description: str | None = None
[docs]
class JsonSchema(BaseModel):
"""Provide a tool definition as a json schema.
While `Tool` is a more user-friendly way to define a tool in
code, in some cases it might put too many constraints on the user. E.g., it can
not be serialized from a json http request. Therefore, function definitions can
also be provided in the serialized, json schema format.
"""
type: Literal["function"] = "function"
function: Function
ToolDefinition = type[Tool] | JsonSchema
"""A tool can either be defined as a Pydantic model or directly as a json schema."""
[docs]
class CodeInterpreter(Tool):
src: str
[docs]
def run(self) -> Any:
global_vars: dict[str, Any] = {}
exec(self.src, global_vars)
return global_vars.get("result")
[docs]
@classmethod
def json_schema(cls) -> dict[str, Any]:
"""Json representation of the code interpreter tool.
This is not passed to the model, but only used for serialization to json
as part of a chat request.
"""
return {"type": "code_interpreter"}
[docs]
class WolframAlpha(Tool):
query: str
[docs]
@staticmethod
def try_from_text(text: str) -> "WolframAlpha | None":
if not text.startswith("wolfram_alpha.call"):
return None
try:
query = text.split('wolfram_alpha.call(query="')[1].split('")')[0].strip()
return WolframAlpha(query=query)
except IndexError:
return None
[docs]
@classmethod
def json_schema(cls) -> dict[str, Any]:
"""Json representation of the wolfram alpha tool.
This is not passed to the model, but only used for serialization to json
as part of a chat request.
"""
return {"type": "wolfram_alpha"}
[docs]
class BraveSearch(Tool):
query: str
[docs]
@staticmethod
def try_from_text(text: str) -> "BraveSearch | None":
if not text.startswith("brave_search.call"):
return None
try:
query = text.split('brave_search.call(query="')[1].split('")')[0].strip()
return BraveSearch(query=query)
except IndexError:
return None
[docs]
@classmethod
def json_schema(cls) -> dict[str, Any]:
"""Json representation of the brave search tool.
This is not passed to the model, but only used for serialization to json
as part of a chat request.
"""
return {"type": "brave_search"}
BuiltInTools: tuple[type[Tool], ...] = (CodeInterpreter, WolframAlpha, BraveSearch)