mirror of
https://github.com/MervinPraison/PraisonAI-Tools.git
synced 2026-11-10 11:41:35 -10:00
Compare commits
No files matched your search
@@ -29,4 +29,53 @@ try {
|
||||
|
||||
assert('PACKAGE_PATHS includes pyproject', rg.PACKAGE_PATHS.includes('pyproject.toml'));
|
||||
|
||||
process.exit(failed ? 1 : 0);
|
||||
(async () => {
|
||||
const noonUtc = new Date('2026-07-08T12:00:00Z');
|
||||
const mockGithub = {
|
||||
rest: {
|
||||
actions: {
|
||||
listWorkflowRuns: async () => ({
|
||||
data: {
|
||||
workflow_runs: [
|
||||
{
|
||||
conclusion: 'success',
|
||||
created_at: '2026-07-08T09:00:00Z',
|
||||
status: 'completed',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
assert('PATCH_RELEASE_INTERVAL_DAYS is 3', rg.PATCH_RELEASE_INTERVAL_DAYS === 3);
|
||||
assert(
|
||||
'hasSuccessfulReleaseWithinDays blocks recent release',
|
||||
await rg.hasSuccessfulReleaseWithinDays(mockGithub, 'o', 'r', noonUtc)
|
||||
);
|
||||
|
||||
const oldReleaseGithub = {
|
||||
rest: {
|
||||
actions: {
|
||||
listWorkflowRuns: async () => ({
|
||||
data: {
|
||||
workflow_runs: [
|
||||
{
|
||||
conclusion: 'success',
|
||||
created_at: '2026-07-04T09:00:00Z',
|
||||
status: 'completed',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
assert(
|
||||
'hasSuccessfulReleaseWithinDays allows after 3 days',
|
||||
!(await rg.hasSuccessfulReleaseWithinDays(oldReleaseGithub, 'o', 'r', noonUtc))
|
||||
);
|
||||
|
||||
process.exit(failed ? 1 : 0);
|
||||
})();
|
||||
@@ -6,6 +6,8 @@ const https = require('https');
|
||||
const config = require('./gate-config.js');
|
||||
|
||||
const PACKAGE_PATHS = config.packagePaths;
|
||||
/** Minimum days between successful patch auto-releases. */
|
||||
const PATCH_RELEASE_INTERVAL_DAYS = 3;
|
||||
const ACTIVE_RELEASE_STATUSES = new Set([
|
||||
'queued', 'in_progress', 'waiting', 'pending', 'requested',
|
||||
]);
|
||||
@@ -61,8 +63,18 @@ function utcDayStart(now = new Date()) {
|
||||
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
||||
}
|
||||
|
||||
async function hasSuccessfulReleaseToday(github, owner, repo, now = new Date()) {
|
||||
const dayStart = utcDayStart(now);
|
||||
function releaseIntervalStart(now = new Date(), intervalDays = PATCH_RELEASE_INTERVAL_DAYS) {
|
||||
return new Date(now.getTime() - intervalDays * 24 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
async function hasSuccessfulReleaseWithinDays(
|
||||
github,
|
||||
owner,
|
||||
repo,
|
||||
now = new Date(),
|
||||
intervalDays = PATCH_RELEASE_INTERVAL_DAYS,
|
||||
) {
|
||||
const windowStart = releaseIntervalStart(now, intervalDays);
|
||||
const runs = await github.rest.actions.listWorkflowRuns({
|
||||
owner,
|
||||
repo,
|
||||
@@ -71,10 +83,15 @@ async function hasSuccessfulReleaseToday(github, owner, repo, now = new Date())
|
||||
per_page: 30,
|
||||
});
|
||||
return runs.data.workflow_runs.some(
|
||||
(r) => r.conclusion === 'success' && new Date(r.created_at) >= dayStart
|
||||
(r) => r.conclusion === 'success' && new Date(r.created_at) >= windowStart
|
||||
);
|
||||
}
|
||||
|
||||
/** @deprecated Use hasSuccessfulReleaseWithinDays — kept for selftests. */
|
||||
async function hasSuccessfulReleaseToday(github, owner, repo, now = new Date()) {
|
||||
return hasSuccessfulReleaseWithinDays(github, owner, repo, now, 1);
|
||||
}
|
||||
|
||||
async function lastGreenCiSha(github, owner, repo) {
|
||||
const runs = await github.rest.actions.listWorkflowRuns({
|
||||
owner,
|
||||
@@ -116,9 +133,11 @@ async function evaluateReleasePreflight(github, owner, repo, options, core) {
|
||||
}
|
||||
|
||||
const referenceTime = options.now instanceof Date ? options.now : new Date();
|
||||
if (await hasSuccessfulReleaseToday(github, owner, repo, referenceTime)) {
|
||||
const day = referenceTime.toISOString().slice(0, 10);
|
||||
reasons.push(`already released today (UTC ${day}); max one patch release per day`);
|
||||
if (await hasSuccessfulReleaseWithinDays(github, owner, repo, referenceTime)) {
|
||||
reasons.push(
|
||||
`already released within last ${PATCH_RELEASE_INTERVAL_DAYS} days; `
|
||||
+ `max one patch release every ${PATCH_RELEASE_INTERVAL_DAYS} days`
|
||||
);
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -196,6 +215,9 @@ module.exports = {
|
||||
bumpPatch,
|
||||
readVersionsFromTree,
|
||||
pypiVersionExists,
|
||||
PATCH_RELEASE_INTERVAL_DAYS,
|
||||
releaseIntervalStart,
|
||||
hasSuccessfulReleaseWithinDays,
|
||||
hasSuccessfulReleaseToday,
|
||||
utcDayStart,
|
||||
evaluateReleasePreflight,
|
||||
|
||||
@@ -2,11 +2,12 @@ name: Nightly Release Gate
|
||||
|
||||
# Preflight before PyPI Release (patch bump).
|
||||
# Primary: CI success on main (workflow_run).
|
||||
# Backstop: nightly cron at 00:00 UTC.
|
||||
# Backstop: cron every 3 days at 00:00 UTC.
|
||||
# Dedupe: max one successful patch release every 3 days (see release-gate.js).
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
- cron: '0 0 */3 * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
|
||||
@@ -133,7 +133,9 @@ jobs:
|
||||
- name: Publish to PyPI
|
||||
if: inputs.dry_run != true
|
||||
env:
|
||||
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN || secrets.PYPI_API_TOKEN }}
|
||||
# PyPI API token auth: username __token__, password = pypi-… token (see pypi.org/help/#invalid-auth)
|
||||
UV_PUBLISH_USERNAME: __token__
|
||||
UV_PUBLISH_PASSWORD: ${{ secrets.PYPI_TOKEN || secrets.PYPI_API_TOKEN }}
|
||||
run: |
|
||||
uv publish
|
||||
|
||||
|
||||
@@ -703,6 +703,19 @@ pip install praisonai-tools[dev]
|
||||
pytest tests/ -v
|
||||
```
|
||||
|
||||
The `dev` extra installs the video/motion-graphics render deps
|
||||
(`playwright`, `imageio-ffmpeg`) so the full suite runs. To exercise the
|
||||
HTML render backend you also need the Chromium browser:
|
||||
|
||||
```bash
|
||||
pip install -e ".[dev]"
|
||||
playwright install chromium
|
||||
pytest tests/ -v
|
||||
```
|
||||
|
||||
If you use a minimal install without those deps, the video tests under
|
||||
`tests/unit/video/` are skipped automatically rather than failing.
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
@@ -194,7 +194,7 @@ def example_team_preset():
|
||||
|
||||
print("✓ Motion graphics team created successfully!")
|
||||
print(f" Agents: {[agent.name for agent in team.agents]}")
|
||||
print(f" Leader: {team.leader.name}")
|
||||
print(f" Coordinator: {team.agents[0].name}")
|
||||
print(f" Workspace: {team._motion_graphics_workspace}")
|
||||
|
||||
except ImportError as e:
|
||||
|
||||
@@ -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",
|
||||
@@ -122,6 +124,7 @@ def __getattr__(name):
|
||||
# Firecrawl
|
||||
"FirecrawlTool": "firecrawl_tool",
|
||||
"firecrawl_scrape": "firecrawl_tool",
|
||||
"firecrawl_search": "firecrawl_tool",
|
||||
# Serper
|
||||
"SerperTool": "serper_tool",
|
||||
"serper_search": "serper_tool",
|
||||
@@ -558,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)
|
||||
@@ -685,6 +692,7 @@ __all__ = [
|
||||
# Firecrawl Tool
|
||||
"FirecrawlTool",
|
||||
"firecrawl_scrape",
|
||||
"firecrawl_search",
|
||||
# Serper Tool
|
||||
"SerperTool",
|
||||
"serper_search",
|
||||
|
||||
@@ -25,7 +25,7 @@ class FirecrawlTool(BaseTool):
|
||||
"""Tool for web scraping using Firecrawl."""
|
||||
|
||||
name = "firecrawl"
|
||||
description = "Scrape and crawl websites using Firecrawl."
|
||||
description = "Scrape, crawl, and search the web using Firecrawl."
|
||||
|
||||
def __init__(self, api_key: Optional[str] = None):
|
||||
self.api_key = api_key or os.getenv("FIRECRAWL_API_KEY")
|
||||
@@ -38,10 +38,12 @@ class FirecrawlTool(BaseTool):
|
||||
if not self.api_key:
|
||||
raise ValueError("FIRECRAWL_API_KEY is required")
|
||||
try:
|
||||
from firecrawl import FirecrawlApp
|
||||
from firecrawl import Firecrawl
|
||||
except ImportError:
|
||||
raise ImportError("firecrawl-py not installed. Install with: pip install firecrawl-py")
|
||||
self._client = FirecrawlApp(api_key=self.api_key)
|
||||
raise ImportError(
|
||||
"firecrawl-py>=4 not installed. Install with: pip install 'firecrawl-py>=4'"
|
||||
)
|
||||
self._client = Firecrawl(api_key=self.api_key)
|
||||
return self._client
|
||||
|
||||
def run(
|
||||
@@ -56,6 +58,8 @@ class FirecrawlTool(BaseTool):
|
||||
return self.scrape(url=url, **kwargs)
|
||||
elif action == "crawl":
|
||||
return self.crawl(url=url, **kwargs)
|
||||
elif action == "search":
|
||||
return self.search(query=kwargs.get("query") or url, limit=kwargs.get("limit", 5))
|
||||
else:
|
||||
return {"error": f"Unknown action: {action}"}
|
||||
|
||||
@@ -68,16 +72,28 @@ class FirecrawlTool(BaseTool):
|
||||
return {"error": "FIRECRAWL_API_KEY not configured"}
|
||||
|
||||
try:
|
||||
params = {}
|
||||
# firecrawl-py v2 (>=4): kwargs instead of a params dict; returns a
|
||||
# typed Document, not a dict.
|
||||
kwargs: Dict[str, Any] = {}
|
||||
if formats:
|
||||
params["formats"] = formats
|
||||
|
||||
result = self.client.scrape_url(url, params=params if params else None)
|
||||
|
||||
kwargs["formats"] = formats
|
||||
|
||||
result = self.client.scrape(url, **kwargs)
|
||||
|
||||
# v2 returns a Document object; metadata is a typed DocumentMetadata.
|
||||
# Coerce to a plain dict to keep this tool's return shape stable.
|
||||
metadata = getattr(result, "metadata", None)
|
||||
if metadata is not None and hasattr(metadata, "model_dump"):
|
||||
metadata = metadata.model_dump(exclude_none=True)
|
||||
elif metadata is None:
|
||||
metadata = {}
|
||||
|
||||
markdown = getattr(result, "markdown", None) or ""
|
||||
|
||||
return {
|
||||
"url": url,
|
||||
"markdown": result.get("markdown", "")[:5000],
|
||||
"metadata": result.get("metadata", {}),
|
||||
"markdown": markdown[:5000],
|
||||
"metadata": metadata,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Firecrawl scrape error: {e}")
|
||||
@@ -92,25 +108,77 @@ class FirecrawlTool(BaseTool):
|
||||
return [{"error": "FIRECRAWL_API_KEY not configured"}]
|
||||
|
||||
try:
|
||||
result = self.client.crawl_url(
|
||||
# `limit` may arrive as a string (e.g. "10") or None when invoked by
|
||||
# an LLM/agent; coerce to int so the SDK call and `data[:limit]` slice
|
||||
# below don't raise TypeError.
|
||||
try:
|
||||
limit = int(limit) if limit is not None else 10
|
||||
except (ValueError, TypeError):
|
||||
limit = 10
|
||||
|
||||
# firecrawl-py v2 (>=4): kwargs instead of a params dict; returns a
|
||||
# typed CrawlJob whose `.data` is a list of Document objects.
|
||||
result = self.client.crawl(
|
||||
url,
|
||||
params={"limit": limit},
|
||||
limit=limit,
|
||||
poll_interval=5,
|
||||
)
|
||||
|
||||
|
||||
data = getattr(result, "data", None) or []
|
||||
|
||||
pages = []
|
||||
for page in result.get("data", [])[:limit]:
|
||||
for page in data[:limit]:
|
||||
metadata = getattr(page, "metadata", None)
|
||||
pages.append({
|
||||
"url": page.get("metadata", {}).get("sourceURL"),
|
||||
"title": page.get("metadata", {}).get("title"),
|
||||
"markdown": page.get("markdown", "")[:2000],
|
||||
"url": getattr(metadata, "source_url", None),
|
||||
"title": getattr(metadata, "title", None),
|
||||
"markdown": (getattr(page, "markdown", None) or "")[:2000],
|
||||
})
|
||||
return pages
|
||||
except Exception as e:
|
||||
logger.error(f"Firecrawl crawl error: {e}")
|
||||
return [{"error": str(e)}]
|
||||
|
||||
def search(self, query: str, limit: int = 5) -> List[Dict[str, Any]]:
|
||||
"""Search the web and return results."""
|
||||
if not query:
|
||||
return [{"error": "query is required"}]
|
||||
|
||||
if not self.api_key:
|
||||
return [{"error": "FIRECRAWL_API_KEY not configured"}]
|
||||
|
||||
try:
|
||||
# `limit` may arrive as a string or None from an LLM/agent; coerce to
|
||||
# int so the SDK call and `web[:limit]` slice below don't raise.
|
||||
try:
|
||||
limit = int(limit) if limit is not None else 5
|
||||
except (ValueError, TypeError):
|
||||
limit = 5
|
||||
|
||||
# firecrawl-py v2 returns a typed SearchData object whose results are
|
||||
# grouped by source; read the web results and coerce to plain dicts.
|
||||
result = self.client.search(query, limit=limit)
|
||||
|
||||
web = getattr(result, "web", None) or []
|
||||
results = []
|
||||
for item in web[:limit]:
|
||||
results.append({
|
||||
"url": getattr(item, "url", None),
|
||||
"title": getattr(item, "title", None),
|
||||
"description": getattr(item, "description", None),
|
||||
"markdown": (getattr(item, "markdown", None) or "")[:2000],
|
||||
})
|
||||
return results
|
||||
except Exception as e:
|
||||
logger.error(f"Firecrawl search error: {e}")
|
||||
return [{"error": str(e)}]
|
||||
|
||||
|
||||
def firecrawl_scrape(url: str) -> Dict[str, Any]:
|
||||
"""Scrape URL with Firecrawl."""
|
||||
return FirecrawlTool().scrape(url=url)
|
||||
|
||||
|
||||
def firecrawl_search(query: str, limit: int = 5) -> List[Dict[str, Any]]:
|
||||
"""Search the web with Firecrawl."""
|
||||
return FirecrawlTool().search(query=query, limit=limit)
|
||||
@@ -35,6 +35,11 @@ def motion_graphics_team(
|
||||
- Researcher: Optional web search for content research
|
||||
- CodeExplorer: Optional git repository exploration
|
||||
- Animator: HTML/GSAP composition authoring and rendering
|
||||
|
||||
The team runs with ``process="hierarchical"``: the ``manager_llm``
|
||||
orchestrates the worker agents at runtime rather than exposing a static
|
||||
``leader`` reference. The coordinator is the semantic leader and is always
|
||||
the first agent in ``team.agents``.
|
||||
|
||||
Args:
|
||||
research: Include research specialist
|
||||
@@ -46,6 +51,12 @@ def motion_graphics_team(
|
||||
|
||||
Returns:
|
||||
AgentTeam configured for motion graphics creation
|
||||
|
||||
Note:
|
||||
The team uses ``process="hierarchical"`` where the ``manager_llm``
|
||||
orchestrates the worker agents at runtime. The coordinator is the
|
||||
semantic leader and is always the first agent (``team.agents[0]``);
|
||||
there is no static ``team.leader`` reference in this mode.
|
||||
"""
|
||||
if AgentTeam is None:
|
||||
raise ImportError(
|
||||
|
||||
+18
-2
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "praisonai-tools"
|
||||
version = "0.2.32"
|
||||
version = "0.2.36"
|
||||
description = "Extended tools for PraisonAI Agents"
|
||||
authors = [
|
||||
{name = "Mervin Praison"}
|
||||
@@ -27,11 +27,21 @@ 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 = [
|
||||
"pytest>=8.0.0",
|
||||
"pytest-asyncio>=0.23.0",
|
||||
"playwright>=1.40",
|
||||
"imageio-ffmpeg>=0.5",
|
||||
]
|
||||
wordpress = [
|
||||
"praisonaiwp[ai]>=0.1.0",
|
||||
@@ -83,4 +93,10 @@ packages = ["praisonai_tools"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
asyncio_mode = "auto"
|
||||
asyncio_mode = "auto"
|
||||
|
||||
[tool.ruff.lint]
|
||||
# Pin the rule set to the historical default (Pyflakes + a subset of
|
||||
# pycodestyle) so an unpinned ruff upgrade does not spuriously fail CI by
|
||||
# enabling thousands of new default rules across the pre-existing codebase.
|
||||
select = ["E4", "E7", "E9", "F"]
|
||||
@@ -164,8 +164,11 @@ class TestMotionGraphicsTeam:
|
||||
custom_param="test"
|
||||
)
|
||||
|
||||
# Check that custom parameters are passed through
|
||||
assert team.kwargs == {"custom_param": "test"}
|
||||
# Hierarchical teams forward process + manager_llm alongside
|
||||
# any user-supplied kwargs, so assert individual entries.
|
||||
assert team.kwargs["custom_param"] == "test"
|
||||
assert team.kwargs["process"] == "hierarchical"
|
||||
assert team.kwargs["manager_llm"] == "gpt-4"
|
||||
|
||||
# Check that workspace is set
|
||||
assert team._motion_graphics_workspace == Path(tmpdir)
|
||||
@@ -209,15 +212,20 @@ class TestMotionGraphicsTeam:
|
||||
@patch('praisonai_tools.video.motion_graphics.team.search_web', MockSearch())
|
||||
@patch('praisonai_tools.video.motion_graphics.team.create_motion_graphics_agent')
|
||||
def test_coordinator_is_leader(self, mock_create_agent):
|
||||
"""Test that coordinator is set as team leader."""
|
||||
"""Test that coordinator is the semantic leader (first agent).
|
||||
|
||||
In hierarchical process mode the manager LLM orchestrates workers,
|
||||
so the coordinator is the first agent rather than a static
|
||||
``team.leader`` reference.
|
||||
"""
|
||||
mock_animator = MockAgent(name="animator")
|
||||
mock_create_agent.return_value = mock_animator
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
team = motion_graphics_team(workspace=tmpdir)
|
||||
|
||||
assert team.leader is not None
|
||||
assert team.leader.name == "coordinator"
|
||||
coordinator = team.agents[0]
|
||||
assert coordinator.name == "coordinator"
|
||||
|
||||
@patch('praisonai_tools.video.motion_graphics.team.AgentTeam', MockAgentTeam)
|
||||
@patch('praisonai_tools.video.motion_graphics.team.Agent', MockAgent)
|
||||
@@ -278,7 +286,7 @@ class TestMotionGraphicsTeam:
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
team = motion_graphics_team(workspace=tmpdir)
|
||||
|
||||
coordinator = team.leader
|
||||
coordinator = team.agents[0]
|
||||
instructions = coordinator.instructions.lower()
|
||||
|
||||
assert "coordinator" in instructions
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Pytest configuration for video/motion-graphics tests.
|
||||
|
||||
Most tests in this tree are fully mocked (protocols, dataclasses, mocked
|
||||
backends) and run without the optional ``video-motion`` dependencies. Only a
|
||||
subset actually constructs :class:`HtmlRenderBackend`, which hard-requires
|
||||
``playwright`` and ``imageio-ffmpeg`` at construction time.
|
||||
|
||||
Rather than skipping the whole tree when those packages are absent (for example
|
||||
a minimal ``pip install -e ".[dev]"`` where the wheels could not be resolved),
|
||||
gate *only* the dependency-bound tests. Mark them with
|
||||
``@pytest.mark.requires_video_deps`` and this hook skips just those when the
|
||||
extras are missing, preserving coverage for the dependency-independent tests.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
|
||||
import pytest
|
||||
|
||||
_VIDEO_DEPS = ("playwright", "imageio_ffmpeg")
|
||||
_MISSING_VIDEO_DEPS = [
|
||||
name for name in _VIDEO_DEPS if importlib.util.find_spec(name) is None
|
||||
]
|
||||
VIDEO_DEPS_AVAILABLE = not _MISSING_VIDEO_DEPS
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
config.addinivalue_line(
|
||||
"markers",
|
||||
"requires_video_deps: mark test as requiring the optional "
|
||||
"video-motion extras (playwright, imageio-ffmpeg).",
|
||||
)
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(config, items):
|
||||
if VIDEO_DEPS_AVAILABLE:
|
||||
return
|
||||
skip_reason = (
|
||||
"video-motion extra not installed "
|
||||
"(pip install -e '.[dev,video-motion]'); missing: "
|
||||
+ ", ".join(_MISSING_VIDEO_DEPS)
|
||||
)
|
||||
skip_marker = pytest.mark.skip(reason=skip_reason)
|
||||
for item in items:
|
||||
if "requires_video_deps" in item.keywords:
|
||||
item.add_marker(skip_marker)
|
||||
@@ -13,6 +13,7 @@ except ImportError:
|
||||
|
||||
|
||||
@pytest.mark.skipif(not BACKEND_AVAILABLE, reason="Motion graphics backend not available")
|
||||
@pytest.mark.requires_video_deps
|
||||
class TestBackendSafety:
|
||||
"""Test workspace safety features."""
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"""Unit tests for HTML render backend."""
|
||||
|
||||
import pytest
|
||||
import tempfile
|
||||
import asyncio
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch, AsyncMock
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from praisonai_tools.video.motion_graphics.backend_html import HtmlRenderBackend
|
||||
from praisonai_tools.video.motion_graphics.protocols import RenderOpts
|
||||
@@ -13,22 +14,32 @@ from praisonai_tools.video.motion_graphics.protocols import RenderOpts
|
||||
class TestHtmlRenderBackend:
|
||||
"""Test HTML render backend."""
|
||||
|
||||
@pytest.mark.requires_video_deps
|
||||
def test_import_error_handling(self):
|
||||
"""Test that import errors are handled properly."""
|
||||
with patch('praisonai_tools.video.motion_graphics.backend_html.async_playwright', None):
|
||||
with pytest.raises(ImportError, match="Playwright not installed"):
|
||||
HtmlRenderBackend()
|
||||
|
||||
with patch('praisonai_tools.video.motion_graphics.backend_html.imageio_ffmpeg', None):
|
||||
with pytest.raises(ImportError, match="imageio-ffmpeg not installed"):
|
||||
HtmlRenderBackend()
|
||||
with (
|
||||
patch('praisonai_tools.video.motion_graphics.backend_html.async_playwright', None),
|
||||
pytest.raises(ImportError, match="Playwright not installed"),
|
||||
):
|
||||
HtmlRenderBackend()
|
||||
|
||||
# Ensure the playwright guard passes so the imageio-ffmpeg guard is the
|
||||
# one under test, regardless of whether the optional deps are installed.
|
||||
with (
|
||||
patch('praisonai_tools.video.motion_graphics.backend_html.async_playwright', Mock()),
|
||||
patch('praisonai_tools.video.motion_graphics.backend_html.imageio_ffmpeg', None),
|
||||
pytest.raises(ImportError, match="imageio-ffmpeg not installed"),
|
||||
):
|
||||
HtmlRenderBackend()
|
||||
|
||||
def test_init_success(self):
|
||||
"""Test successful initialization."""
|
||||
with patch('praisonai_tools.video.motion_graphics.backend_html.async_playwright', Mock()):
|
||||
with patch('praisonai_tools.video.motion_graphics.backend_html.imageio_ffmpeg', Mock()):
|
||||
backend = HtmlRenderBackend()
|
||||
assert backend is not None
|
||||
with (
|
||||
patch('praisonai_tools.video.motion_graphics.backend_html.async_playwright', Mock()),
|
||||
patch('praisonai_tools.video.motion_graphics.backend_html.imageio_ffmpeg', Mock()),
|
||||
):
|
||||
backend = HtmlRenderBackend()
|
||||
assert backend is not None
|
||||
|
||||
|
||||
class TestLinting:
|
||||
@@ -36,9 +47,11 @@ class TestLinting:
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
with patch('praisonai_tools.video.motion_graphics.backend_html.async_playwright', Mock()):
|
||||
with patch('praisonai_tools.video.motion_graphics.backend_html.imageio_ffmpeg', Mock()):
|
||||
self.backend = HtmlRenderBackend()
|
||||
with (
|
||||
patch('praisonai_tools.video.motion_graphics.backend_html.async_playwright', Mock()),
|
||||
patch('praisonai_tools.video.motion_graphics.backend_html.imageio_ffmpeg', Mock()),
|
||||
):
|
||||
self.backend = HtmlRenderBackend()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lint_missing_index_html(self):
|
||||
@@ -198,9 +211,11 @@ class TestRendering:
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
with patch('praisonai_tools.video.motion_graphics.backend_html.async_playwright', Mock()):
|
||||
with patch('praisonai_tools.video.motion_graphics.backend_html.imageio_ffmpeg', Mock()):
|
||||
self.backend = HtmlRenderBackend()
|
||||
with (
|
||||
patch('praisonai_tools.video.motion_graphics.backend_html.async_playwright', Mock()),
|
||||
patch('praisonai_tools.video.motion_graphics.backend_html.imageio_ffmpeg', Mock()),
|
||||
):
|
||||
self.backend = HtmlRenderBackend()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_render_missing_index_html(self):
|
||||
@@ -321,8 +336,10 @@ class TestRendering:
|
||||
mock_process.returncode = 0
|
||||
mock_subprocess.return_value = mock_process
|
||||
|
||||
with patch.object(self.backend, '_get_ffmpeg_path', return_value='ffmpeg'):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
with (
|
||||
patch.object(self.backend, '_get_ffmpeg_path', return_value='ffmpeg'),
|
||||
tempfile.TemporaryDirectory() as tmpdir,
|
||||
):
|
||||
# Create fake frame files
|
||||
frame_dir = Path(tmpdir)
|
||||
frame_paths = []
|
||||
@@ -357,8 +374,10 @@ class TestRendering:
|
||||
mock_process.returncode = 1
|
||||
mock_subprocess.return_value = mock_process
|
||||
|
||||
with patch.object(self.backend, '_get_ffmpeg_path', return_value='ffmpeg'):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
with (
|
||||
patch.object(self.backend, '_get_ffmpeg_path', return_value='ffmpeg'),
|
||||
tempfile.TemporaryDirectory() as tmpdir,
|
||||
):
|
||||
frame_path = Path(tmpdir) / "frame_000000.png"
|
||||
frame_path.write_bytes(b"fake")
|
||||
|
||||
@@ -378,8 +397,10 @@ class TestRendering:
|
||||
mock_process.kill = Mock()
|
||||
mock_subprocess.return_value = mock_process
|
||||
|
||||
with patch.object(self.backend, '_get_ffmpeg_path', return_value='ffmpeg'):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
with (
|
||||
patch.object(self.backend, '_get_ffmpeg_path', return_value='ffmpeg'),
|
||||
tempfile.TemporaryDirectory() as tmpdir,
|
||||
):
|
||||
frame_path = Path(tmpdir) / "frame_000000.png"
|
||||
frame_path.write_bytes(b"fake")
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@ from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from praisonai_tools.video.motion_graphics.agent import (
|
||||
create_motion_graphics_agent,
|
||||
RenderTools,
|
||||
_resolve_backend
|
||||
_resolve_backend,
|
||||
create_motion_graphics_agent,
|
||||
)
|
||||
from praisonai_tools.video.motion_graphics.protocols import RenderResult, LintResult
|
||||
from praisonai_tools.video.motion_graphics.protocols import LintResult, RenderResult
|
||||
|
||||
|
||||
class MockAgent:
|
||||
@@ -96,6 +96,7 @@ class TestRenderTools:
|
||||
class TestResolveBackend:
|
||||
"""Test backend resolution."""
|
||||
|
||||
@pytest.mark.requires_video_deps
|
||||
def test_resolve_string_backend(self):
|
||||
"""Test resolving string backend specification."""
|
||||
backend = _resolve_backend("html")
|
||||
@@ -119,6 +120,7 @@ class TestResolveBackend:
|
||||
class TestCreateMotionGraphicsAgent:
|
||||
"""Test motion graphics agent factory."""
|
||||
|
||||
@pytest.mark.requires_video_deps
|
||||
@patch('praisonai_tools.video.motion_graphics.agent.Agent', MockAgent)
|
||||
@patch('praisonai_tools.video.motion_graphics.agent.FileTools', MockFileTools)
|
||||
def test_create_agent_defaults(self):
|
||||
@@ -137,6 +139,7 @@ class TestCreateMotionGraphicsAgent:
|
||||
"lint_composition", "render_composition"} <= tool_names
|
||||
assert "motion graphics specialist" in agent.instructions.lower()
|
||||
|
||||
@pytest.mark.requires_video_deps
|
||||
@patch('praisonai_tools.video.motion_graphics.agent.Agent', MockAgent)
|
||||
@patch('praisonai_tools.video.motion_graphics.agent.FileTools', MockFileTools)
|
||||
def test_create_agent_custom_params(self):
|
||||
@@ -159,6 +162,7 @@ class TestCreateMotionGraphicsAgent:
|
||||
with pytest.raises(ImportError, match="praisonaiagents not available"):
|
||||
create_motion_graphics_agent()
|
||||
|
||||
@pytest.mark.requires_video_deps
|
||||
@patch('praisonai_tools.video.motion_graphics.agent.Agent', MockAgent)
|
||||
@patch('praisonai_tools.video.motion_graphics.agent.FileTools', MockFileTools)
|
||||
def test_create_agent_auto_workspace(self):
|
||||
@@ -183,6 +187,7 @@ class TestCreateMotionGraphicsAgent:
|
||||
|
||||
assert agent._motion_graphics_backend is mock_backend
|
||||
|
||||
@pytest.mark.requires_video_deps
|
||||
@patch('praisonai_tools.video.motion_graphics.agent.Agent', MockAgent)
|
||||
@patch('praisonai_tools.video.motion_graphics.agent.FileTools', MockFileTools)
|
||||
def test_create_agent_workspace_creation(self):
|
||||
@@ -195,6 +200,7 @@ class TestCreateMotionGraphicsAgent:
|
||||
assert workspace_path.exists()
|
||||
assert agent._motion_graphics_workspace == workspace_path
|
||||
|
||||
@pytest.mark.requires_video_deps
|
||||
@patch('praisonai_tools.video.motion_graphics.agent.Agent', MockAgent)
|
||||
@patch('praisonai_tools.video.motion_graphics.agent.FileTools', MockFileTools)
|
||||
def test_create_agent_skill_included(self):
|
||||
@@ -207,6 +213,7 @@ class TestCreateMotionGraphicsAgent:
|
||||
assert "timeline" in agent.instructions
|
||||
assert "window.__timelines" in agent.instructions
|
||||
|
||||
@pytest.mark.requires_video_deps
|
||||
@patch('praisonai_tools.video.motion_graphics.agent.Agent', MockAgent)
|
||||
@patch('praisonai_tools.video.motion_graphics.agent.FileTools', MockFileTools)
|
||||
def test_create_agent_output_validation(self):
|
||||
@@ -219,6 +226,7 @@ class TestCreateMotionGraphicsAgent:
|
||||
assert "Never fabricate file paths" in agent.instructions
|
||||
assert "3 failed attempts" in agent.instructions
|
||||
|
||||
@pytest.mark.requires_video_deps
|
||||
@patch('praisonai_tools.video.motion_graphics.agent.Agent', MockAgent)
|
||||
@patch('praisonai_tools.video.motion_graphics.agent.FileTools', MockFileTools)
|
||||
def test_agent_tools_configuration(self):
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from praisonai_tools.video.motion_graphics.protocols import (
|
||||
LintResult,
|
||||
RenderBackendProtocol,
|
||||
RenderOpts,
|
||||
LintResult,
|
||||
RenderResult,
|
||||
RenderBackendProtocol
|
||||
)
|
||||
|
||||
|
||||
@@ -112,6 +112,7 @@ class TestRenderResult:
|
||||
class TestRenderBackendProtocol:
|
||||
"""Test RenderBackendProtocol protocol."""
|
||||
|
||||
@pytest.mark.requires_video_deps
|
||||
def test_protocol_runtime_checkable(self):
|
||||
"""Test that protocol is runtime checkable."""
|
||||
from praisonai_tools.video.motion_graphics.backend_html import HtmlRenderBackend
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
"""Tests for render retry functionality."""
|
||||
|
||||
import pytest
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
try:
|
||||
from praisonai_tools.video.motion_graphics.agent import RenderTools
|
||||
from praisonai_tools.video.motion_graphics.protocols import RenderResult, RenderOpts
|
||||
from praisonai_tools.video.motion_graphics.protocols import RenderOpts, RenderResult
|
||||
AGENT_AVAILABLE = True
|
||||
except ImportError:
|
||||
AGENT_AVAILABLE = False
|
||||
|
||||
Reference in new issue
Block a user