mirror of
https://github.com/MervinPraison/PraisonAI-Tools.git
synced 2026-11-13 05:01:37 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e1fe1d860 | ||
|
|
0629d18a4e | ||
|
|
1950368530 |
No files matched your search
@@ -37,6 +37,11 @@ from importlib.metadata import version, PackageNotFoundError
|
||||
|
||||
from praisonai_tools.tools.base import BaseTool, ToolResult, ToolValidationError, validate_tool
|
||||
from praisonai_tools.tools.decorator import tool, FunctionTool, is_tool, get_tool_schema
|
||||
from praisonai_tools.catalogue import (
|
||||
ToolCatalogueEntry,
|
||||
list_tools,
|
||||
get_tool_names,
|
||||
)
|
||||
|
||||
try:
|
||||
__version__ = version("praisonai-tools")
|
||||
@@ -64,6 +69,10 @@ __all__ = [
|
||||
"FunctionTool",
|
||||
"is_tool",
|
||||
"get_tool_schema",
|
||||
# Tool discovery / catalogue
|
||||
"ToolCatalogueEntry",
|
||||
"list_tools",
|
||||
"get_tool_names",
|
||||
# Email Tool
|
||||
"EmailTool",
|
||||
"send_email",
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
"""Tool discovery / catalogue contract for PraisonAI Tools.
|
||||
|
||||
This module provides a single, programmatic way to enumerate every tool that
|
||||
``praisonai-tools`` ships, plus any third-party tool registered via the
|
||||
``praisonai.tools`` entry-point group. It exists so that the wider SDK (CLI,
|
||||
docs, YAML validators, third-party packages) can answer *"what tools can I
|
||||
actually use here?"* against a live catalogue derived from the real registry
|
||||
rather than a hand-maintained allowlist that drifts.
|
||||
|
||||
Usage:
|
||||
from praisonai_tools import list_tools
|
||||
|
||||
for entry in list_tools():
|
||||
print(entry.name, entry.module, entry.extras, entry.summary)
|
||||
|
||||
Third-party packages can register their own tools by declaring an entry point:
|
||||
|
||||
# pyproject.toml (third-party tool package)
|
||||
[project.entry-points."praisonai.tools"]
|
||||
my_tool = "my_pkg:MyTool"
|
||||
|
||||
Nothing here imports optional/heavy dependencies: the catalogue is built from
|
||||
the module map alone, so ``list_tools()`` is safe to call without any extras
|
||||
installed.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Tuple
|
||||
|
||||
ENTRY_POINT_GROUP = "praisonai.tools"
|
||||
|
||||
# Map from the module (as recorded in ``_TOOL_MAP``) to the optional-dependency
|
||||
# extras required to use tools defined in it. Modules absent from this mapping
|
||||
# have no extra requirements (they rely only on the standard library or on the
|
||||
# base dependencies). Keep this aligned with ``[project.optional-dependencies]``
|
||||
# in ``pyproject.toml``.
|
||||
_MODULE_EXTRAS = {
|
||||
"wordpress_tool": ("wordpress",),
|
||||
"pinchwork_tool": ("pinchwork", "marketplace"),
|
||||
"agentid_tool": ("agentid", "marketplace"),
|
||||
"joy_trust_tool": ("joy-trust", "marketplace"),
|
||||
"agentfolio_tool": ("agentfolio", "marketplace"),
|
||||
"praisonai_tools.n8n.n8n_workflow": ("n8n",),
|
||||
"langextract_tool": ("langextract",),
|
||||
"swarmscore_tool": ("swarmscore",),
|
||||
"composio_tool": ("composio",),
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ToolCatalogueEntry:
|
||||
"""A single discoverable tool.
|
||||
|
||||
Attributes:
|
||||
name: Public name used to import/reference the tool, e.g. ``"QdrantTool"``.
|
||||
module: Dotted or relative module path that defines the tool.
|
||||
extras: Optional-dependency extras required to use the tool, e.g.
|
||||
``("swarmscore",)``. Empty when no extra install is needed.
|
||||
summary: A one-line human-readable description of the tool.
|
||||
"""
|
||||
|
||||
name: str
|
||||
module: str
|
||||
extras: Tuple[str, ...] = field(default_factory=tuple)
|
||||
summary: str = ""
|
||||
|
||||
|
||||
def _summary_for(name: str) -> str:
|
||||
"""Derive a concise one-line summary for a tool class name.
|
||||
|
||||
We avoid importing the tool (which could pull heavy/optional deps) and
|
||||
instead produce a stable, human-friendly summary from the name itself.
|
||||
"""
|
||||
if name.endswith("Tool"):
|
||||
base = name[:-4]
|
||||
else:
|
||||
base = name
|
||||
return f"{base} integration tool".strip()
|
||||
|
||||
|
||||
def _first_party_entries() -> "list[ToolCatalogueEntry]":
|
||||
"""Build catalogue entries for every first-party tool class.
|
||||
|
||||
The source of truth is ``praisonai_tools.tools._TOOL_MAP`` which maps every
|
||||
public name (both classes and helper functions) to its defining module. We
|
||||
expose only the tool *classes* (names ending in ``Tool``) in the catalogue,
|
||||
since those are what ``agents.yaml`` and the SDK reference.
|
||||
"""
|
||||
from praisonai_tools.tools import _TOOL_MAP
|
||||
|
||||
entries = []
|
||||
for name, module in sorted(_TOOL_MAP.items()):
|
||||
if not name.endswith("Tool"):
|
||||
continue
|
||||
extras = _MODULE_EXTRAS.get(module, ())
|
||||
entries.append(
|
||||
ToolCatalogueEntry(
|
||||
name=name,
|
||||
module=module,
|
||||
extras=extras,
|
||||
summary=_summary_for(name),
|
||||
)
|
||||
)
|
||||
return entries
|
||||
|
||||
|
||||
def _entry_point_entries() -> "list[ToolCatalogueEntry]":
|
||||
"""Build catalogue entries for third-party tools registered via entry points.
|
||||
|
||||
Third-party packages register under the ``praisonai.tools`` entry-point
|
||||
group. We do not import the target object (to keep discovery cheap and
|
||||
dependency-free); we record the declared name and module value instead.
|
||||
"""
|
||||
try:
|
||||
from importlib.metadata import entry_points
|
||||
except ImportError: # pragma: no cover - Python < 3.8
|
||||
return []
|
||||
|
||||
entries = []
|
||||
try:
|
||||
eps = entry_points()
|
||||
# importlib.metadata API differs across versions.
|
||||
if hasattr(eps, "select"):
|
||||
selected = eps.select(group=ENTRY_POINT_GROUP)
|
||||
else: # pragma: no cover - Python < 3.10 dict interface
|
||||
selected = eps.get(ENTRY_POINT_GROUP, [])
|
||||
except Exception: # pragma: no cover - defensive
|
||||
return []
|
||||
|
||||
for ep in selected:
|
||||
entries.append(
|
||||
ToolCatalogueEntry(
|
||||
name=ep.name,
|
||||
module=getattr(ep, "value", "") or "",
|
||||
extras=(),
|
||||
summary=f"Third-party tool registered via {ENTRY_POINT_GROUP}",
|
||||
)
|
||||
)
|
||||
return entries
|
||||
|
||||
|
||||
def list_tools() -> "list[ToolCatalogueEntry]":
|
||||
"""Return the full catalogue of available tools.
|
||||
|
||||
Includes every first-party tool shipped with ``praisonai-tools`` plus any
|
||||
third-party tool registered via the ``praisonai.tools`` entry-point group.
|
||||
Entries are de-duplicated by name (first-party wins) and returned sorted by
|
||||
name.
|
||||
"""
|
||||
seen = {}
|
||||
for entry in _first_party_entries():
|
||||
seen[entry.name] = entry
|
||||
for entry in _entry_point_entries():
|
||||
seen.setdefault(entry.name, entry)
|
||||
return sorted(seen.values(), key=lambda e: e.name)
|
||||
|
||||
|
||||
def get_tool_names() -> "set[str]":
|
||||
"""Return the set of every available tool name.
|
||||
|
||||
Convenience helper for validators that only need membership checks (e.g. to
|
||||
replace a hand-maintained allowlist).
|
||||
"""
|
||||
return {entry.name for entry in list_tools()}
|
||||
+18
-1
@@ -11,6 +11,7 @@ def main():
|
||||
print("PraisonAI Developer Tools CLI")
|
||||
print("\nUsage: praisonai-tools [tool] [args]")
|
||||
print("\nAvailable tools:")
|
||||
print(" list List the available tool catalogue")
|
||||
print(" docs-generate Generate SDK reference documentation")
|
||||
print(" video AI-powered video editing tools")
|
||||
print(" fcp Final Cut Pro automation tools")
|
||||
@@ -19,7 +20,23 @@ def main():
|
||||
tool = sys.argv[1]
|
||||
tool_args = sys.argv[2:]
|
||||
|
||||
if tool == "docs-generate":
|
||||
if tool == "list":
|
||||
parser = argparse.ArgumentParser(prog="praisonai-tools list")
|
||||
parser.add_argument("--extras", action="store_true",
|
||||
help="Only show tools that require optional-dependency extras")
|
||||
args = parser.parse_args(tool_args)
|
||||
from .catalogue import list_tools
|
||||
entries = list_tools()
|
||||
if args.extras:
|
||||
entries = [e for e in entries if e.extras]
|
||||
width = max((len(e.name) for e in entries), default=0)
|
||||
for entry in entries:
|
||||
extras = f" [extras: {', '.join(entry.extras)}]" if entry.extras else ""
|
||||
print(f"{entry.name.ljust(width)} {entry.summary}{extras}")
|
||||
print(f"\n{len(entries)} tools")
|
||||
return 0
|
||||
|
||||
elif tool == "docs-generate":
|
||||
# Handle docs-generate with its own parser for better help
|
||||
parser = argparse.ArgumentParser(prog="praisonai-tools docs-generate")
|
||||
parser.add_argument("--package", choices=["praisonaiagents", "praisonai", "typescript", "all"],
|
||||
|
||||
@@ -19,10 +19,12 @@ Ready-to-use tools:
|
||||
from praisonai_tools.tools.base import BaseTool, ToolResult, ToolValidationError, validate_tool
|
||||
from praisonai_tools.tools.decorator import tool, FunctionTool, is_tool, get_tool_schema
|
||||
|
||||
# Lazy imports for tools to avoid loading dependencies until needed
|
||||
def __getattr__(name):
|
||||
"""Lazy load tool classes."""
|
||||
tool_map = {
|
||||
# Mapping of every public tool name (classes and functions) to the module that
|
||||
# defines it. This is the single source of truth for lazy loading AND for the
|
||||
# catalogue/discovery contract (see ``praisonai_tools.catalogue``). Keeping it a
|
||||
# module-level constant lets other modules enumerate the shipped tools without
|
||||
# importing any optional dependency.
|
||||
_TOOL_MAP = {
|
||||
# Email
|
||||
"EmailTool": "email_tool",
|
||||
"send_email": "email_tool",
|
||||
@@ -559,10 +561,14 @@ def __getattr__(name):
|
||||
"scan_agent_code": "inkog_tool",
|
||||
"scan_skill_package": "inkog_tool",
|
||||
"scan_mcp_server": "inkog_tool",
|
||||
}
|
||||
}
|
||||
|
||||
if name in tool_map:
|
||||
module_name = tool_map[name]
|
||||
|
||||
# Lazy imports for tools to avoid loading dependencies until needed
|
||||
def __getattr__(name):
|
||||
"""Lazy load tool classes."""
|
||||
if name in _TOOL_MAP:
|
||||
module_name = _TOOL_MAP[name]
|
||||
from importlib import import_module
|
||||
|
||||
# Handle absolute module paths (e.g., praisonai_tools.n8n.n8n_workflow)
|
||||
|
||||
+9
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "praisonai-tools"
|
||||
version = "0.2.35"
|
||||
version = "0.2.36"
|
||||
description = "Extended tools for PraisonAI Agents"
|
||||
authors = [
|
||||
{name = "Mervin Praison"}
|
||||
@@ -27,6 +27,14 @@ dependencies = [
|
||||
[project.scripts]
|
||||
praisonai-tools = "praisonai_tools.cli:main"
|
||||
|
||||
# Discovery contract for third-party tool packages. They register agent-callable
|
||||
# tools under the ``praisonai.tools`` entry-point group so that
|
||||
# ``pip install some-praisonai-tool`` makes them discoverable via
|
||||
# ``praisonai_tools.list_tools()`` without editing core, e.g.:
|
||||
#
|
||||
# [project.entry-points."praisonai.tools"]
|
||||
# my_tool = "my_pkg:MyTool"
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
dev = [
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Tests for the tool discovery / catalogue contract."""
|
||||
|
||||
|
||||
def test_list_tools_importable_from_top_level():
|
||||
from praisonai_tools import list_tools, ToolCatalogueEntry, get_tool_names
|
||||
|
||||
assert callable(list_tools)
|
||||
assert callable(get_tool_names)
|
||||
assert ToolCatalogueEntry is not None
|
||||
|
||||
|
||||
def test_list_tools_returns_entries():
|
||||
from praisonai_tools import list_tools
|
||||
from praisonai_tools.catalogue import ToolCatalogueEntry
|
||||
|
||||
entries = list_tools()
|
||||
assert isinstance(entries, list)
|
||||
assert len(entries) > 100 # ships a large catalogue
|
||||
assert all(isinstance(e, ToolCatalogueEntry) for e in entries)
|
||||
|
||||
|
||||
def test_entries_only_expose_tool_classes():
|
||||
from praisonai_tools import list_tools
|
||||
|
||||
for entry in list_tools():
|
||||
assert entry.name.endswith("Tool"), entry.name
|
||||
|
||||
|
||||
def test_entries_are_sorted_and_unique():
|
||||
from praisonai_tools import list_tools
|
||||
|
||||
names = [e.name for e in list_tools()]
|
||||
assert names == sorted(names)
|
||||
assert len(names) == len(set(names))
|
||||
|
||||
|
||||
def test_catalogue_derived_from_tool_map_source_of_truth():
|
||||
from praisonai_tools import list_tools
|
||||
from praisonai_tools.tools import _TOOL_MAP
|
||||
|
||||
expected = {name for name in _TOOL_MAP if name.endswith("Tool")}
|
||||
actual = {e.name for e in list_tools()}
|
||||
# Every class in the map must be catalogued (third-party EPs may add more).
|
||||
assert expected <= actual
|
||||
|
||||
|
||||
def test_previously_missing_optional_tool_is_now_discoverable():
|
||||
# QdrantTool was a valid tool absent from the wrapper's hand-maintained
|
||||
# allowlist; it must now be discoverable via the catalogue.
|
||||
from praisonai_tools import get_tool_names
|
||||
|
||||
names = get_tool_names()
|
||||
assert "QdrantTool" in names
|
||||
|
||||
|
||||
def test_extras_are_populated_for_optional_tools():
|
||||
from praisonai_tools import list_tools
|
||||
|
||||
by_name = {e.name for e in list_tools()}
|
||||
entries = {e.name: e for e in list_tools()}
|
||||
|
||||
assert "SwarmScoreTool" in by_name
|
||||
assert "swarmscore" in entries["SwarmScoreTool"].extras
|
||||
|
||||
|
||||
def test_entries_have_summary_and_module():
|
||||
from praisonai_tools import list_tools
|
||||
|
||||
for entry in list_tools():
|
||||
assert entry.module
|
||||
assert entry.summary
|
||||
|
||||
|
||||
def test_list_tools_does_not_require_optional_deps():
|
||||
# Building the catalogue must not import any tool module / optional dep.
|
||||
from praisonai_tools import list_tools
|
||||
|
||||
entries = list_tools()
|
||||
assert entries # smoke: no ImportError raised
|
||||
Reference in new issue
Block a user