Added - `arcade dev` - serves a simple fastapi actor - `arcade config` - show/edit/change config in `~/.arcade` - `arcade chat` - chat with LLM without toolcalls Changed: - `arcade show`, `arcade run` - can now use all installed toolkits --------- Co-authored-by: Nate Barbettini <nate@arcade-ai.com>
140 lines
4.7 KiB
Python
140 lines
4.7 KiB
Python
import importlib.metadata
|
|
import importlib.util
|
|
import os
|
|
import types
|
|
from collections import defaultdict
|
|
from pathlib import Path
|
|
|
|
from pydantic import BaseModel, ConfigDict, field_validator
|
|
|
|
from arcade.core.parse import get_tools_from_file
|
|
|
|
|
|
class Toolkit(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
name: str
|
|
"""Name of the toolkit"""
|
|
|
|
package_name: str
|
|
"""Name of the package holding the toolkit"""
|
|
|
|
tools: dict[str, list[str]] = defaultdict(list)
|
|
"""Mapping of module names to tools"""
|
|
|
|
# Other python package metadata
|
|
version: str
|
|
description: str
|
|
author: list[str] = []
|
|
repository: str | None = None
|
|
homepage: str | None = None
|
|
|
|
@field_validator("name", mode="before")
|
|
def strip_arcade_prefix(cls, value: str) -> str:
|
|
"""
|
|
Validator to strip the 'arcade_' prefix from the name if it exists.
|
|
"""
|
|
if value.startswith("arcade_"):
|
|
return value[len("arcade_") :]
|
|
return value
|
|
|
|
@classmethod
|
|
def from_module(cls, module: types.ModuleType) -> "Toolkit":
|
|
"""
|
|
Load a toolkit from an imported python module
|
|
|
|
>>> import arcade_arithmetic
|
|
>>> toolkit = Toolkit.from_module(arcade_arithmetic)
|
|
"""
|
|
return cls.from_package(module.__name__)
|
|
|
|
@classmethod
|
|
def from_package(cls, package: str) -> "Toolkit":
|
|
"""
|
|
Load a Toolkit from a Python package
|
|
"""
|
|
try:
|
|
metadata = importlib.metadata.metadata(package)
|
|
name = metadata["Name"]
|
|
package_name = package
|
|
version = metadata["Version"]
|
|
description = metadata.get("Summary", "") # type: ignore[attr-defined]
|
|
author = metadata.get_all("Author-email")
|
|
homepage = metadata.get("Home-page", None) # type: ignore[attr-defined]
|
|
repo = metadata.get("Repository", None) # type: ignore[attr-defined]
|
|
|
|
except importlib.metadata.PackageNotFoundError as e:
|
|
raise ValueError(f"Package {package} not found.") from e
|
|
except KeyError as e:
|
|
raise ValueError(f"Metadata key error for package {package}.") from e
|
|
except Exception as e:
|
|
raise ValueError(f"Failed to load metadata for package {package}.") from e
|
|
|
|
# Get the package directory
|
|
try:
|
|
package_dir = Path(get_package_directory(package))
|
|
except AttributeError as e:
|
|
raise ValueError(f"Failed to locate package directory for {package}.") from e
|
|
|
|
# Get all python files in the package directory
|
|
try:
|
|
modules = [f for f in package_dir.glob("**/*.py") if f.is_file()]
|
|
except OSError as e:
|
|
raise ValueError(
|
|
f"Failed to locate Python files in package directory for {package}."
|
|
) from e
|
|
|
|
toolkit = cls(
|
|
name=name,
|
|
package_name=package_name,
|
|
version=version,
|
|
description=description,
|
|
author=author if author else [],
|
|
homepage=homepage,
|
|
repository=repo,
|
|
)
|
|
|
|
for module_path in modules:
|
|
relative_path = module_path.relative_to(package_dir)
|
|
import_path = ".".join(relative_path.with_suffix("").parts)
|
|
import_path = f"{package_name}.{import_path}"
|
|
toolkit.tools[import_path] = get_tools_from_file(str(module_path))
|
|
|
|
if not toolkit.tools:
|
|
raise ValueError(f"No tools found in package {package}")
|
|
|
|
return toolkit
|
|
|
|
@classmethod
|
|
def find_all_arcade_toolkits(cls) -> list["Toolkit"]:
|
|
"""
|
|
Find all installed packages prefixed with 'arcade_' and load them as Toolkits.
|
|
|
|
Returns:
|
|
List[Toolkit]: A list of Toolkit instances.
|
|
"""
|
|
arcade_packages = [
|
|
dist.metadata["Name"]
|
|
for dist in importlib.metadata.distributions()
|
|
if dist.metadata["Name"].startswith("arcade_")
|
|
]
|
|
return [cls.from_package(package) for package in arcade_packages]
|
|
|
|
|
|
def get_package_directory(package_name: str) -> str:
|
|
"""
|
|
Get the directory of a Python package
|
|
"""
|
|
|
|
spec = importlib.util.find_spec(package_name)
|
|
if spec is None:
|
|
raise ImportError(f"Cannot find package named {package_name}")
|
|
|
|
if spec.origin:
|
|
# If the package has an origin, return the directory of the origin
|
|
return os.path.dirname(spec.origin)
|
|
elif spec.submodule_search_locations:
|
|
# If the package is a namespace package, return the first search location
|
|
return spec.submodule_search_locations[0]
|
|
else:
|
|
raise ImportError(f"Package {package_name} does not have a file path associated with it")
|