refactor(apify): move Actor tools into bundled plugin
Re-shelve the three Apify tools from core into plugins/apify/, matching
the Spotify plugin pattern for optional third-party SaaS integrations.
tools/ is reserved for foundational capabilities; third-party service
integrations live in plugins/.
- plugins/apify/{__init__,tools,client}.py + plugin.yaml + README
(kind: backend, auto-loads; registers via ctx.register_tool())
- remove apify_* from _HERMES_CORE_TOOLS in toolsets.py
(TOOLSETS["apify"] entry kept, mirroring spotify)
- tests moved tests/tools/test_apify_tool.py -> tests/plugins/test_apify.py,
import paths updated (34 tests pass)
- add JanHranicky to AUTHOR_MAP in scripts/release.py (CI gate)
The tools_config.py / config.py / lazy_deps.py / pyproject.toml setup +
config UX from the original commit is retained unchanged.
Co-authored-by: JanHranicky <jan.hranicky@seznam.cz>
This commit is contained in:
parent
58e921a819
commit
c97f0a6c82
41
plugins/apify/README.md
Normal file
41
plugins/apify/README.md
Normal file
@ -0,0 +1,41 @@
|
||||
# Apify Actor Tools
|
||||
|
||||
Bundled plugin that brings [Apify](https://apify.com/) Actors into Hermes. Apify
|
||||
hosts 20,000+ ready-made Actors for web automation and data extraction
|
||||
(Instagram, YouTube, Google Maps, LinkedIn, Amazon, and more). The agent can
|
||||
discover the right Actor, inspect its input schema, run it, and collect
|
||||
structured results.
|
||||
|
||||
## Tools
|
||||
|
||||
| Tool | What it does |
|
||||
|------|--------------|
|
||||
| `apify_discover` | Search the Apify Store by keyword, or fetch a specific Actor's input schema + README by `actor_id`. |
|
||||
| `apify_start` | Fire-and-forget batch Actor starts (up to 10 per call). Returns run refs immediately so the agent keeps reasoning while Actors run. |
|
||||
| `apify_collect` | Poll run statuses and return completed dataset results, wrapped in `EXTERNAL_UNTRUSTED_CONTENT` markers. Supports `limit` / `may_have_more` pagination. |
|
||||
|
||||
## Setup
|
||||
|
||||
1. Create an Apify account and get a token at
|
||||
<https://apify.com/account/integrations>.
|
||||
2. Run `hermes tools`, open **Apify Actors**, enable the toolset, and paste your
|
||||
token. (The token is stored in `~/.hermes/.env` as `APIFY_API_TOKEN` and is
|
||||
never sent to the model.)
|
||||
|
||||
The `apify` toolset is **off by default**. The three tools register on startup
|
||||
but stay invisible to the model until `APIFY_API_TOKEN` is set (runtime
|
||||
`check_fn` gate).
|
||||
|
||||
## Architecture
|
||||
|
||||
This is a bundled `kind: backend` plugin (auto-loads, no opt-in), modeled on the
|
||||
Spotify plugin. Tool registration goes through the plugin API
|
||||
(`ctx.register_tool()`), so the Apify tools never enter `_HERMES_CORE_TOOLS`. The
|
||||
`apify-client` SDK is installed on demand via `tools.lazy_deps` (`search.apify`)
|
||||
the first time an Actor runs.
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `__init__.py` | `register(ctx)` — wires the three tools via `ctx.register_tool()`. |
|
||||
| `tools.py` | Handlers + JSON schemas. |
|
||||
| `client.py` | Lazy `apify-client` import, token validation, module-level cache. |
|
||||
72
plugins/apify/__init__.py
Normal file
72
plugins/apify/__init__.py
Normal file
@ -0,0 +1,72 @@
|
||||
"""Apify Actor execution plugin — bundled, auto-loaded.
|
||||
|
||||
Registers three tools (``apify_discover``, ``apify_start``, ``apify_collect``)
|
||||
into the ``apify`` toolset. Each tool is gated by ``_check_token()`` — when the
|
||||
user has not set ``APIFY_API_TOKEN`` the tools stay registered (so they appear
|
||||
in ``hermes tools``) but the runtime check prevents dispatch.
|
||||
|
||||
Why a plugin instead of top-level ``tools/`` files?
|
||||
|
||||
- ``plugins/`` is where third-party service integrations live (see
|
||||
``plugins/spotify/`` for the same pattern — optional SaaS, token-gated,
|
||||
default-off toolset). ``tools/`` is reserved for foundational capabilities
|
||||
(terminal, read_file, web_search, etc.).
|
||||
- Bundled + ``kind: backend`` auto-loads on startup just like the Spotify
|
||||
plugin — no user opt-in needed, no ``plugins.enabled`` config.
|
||||
- Keeps the three Apify tools out of ``_HERMES_CORE_TOOLS`` in ``toolsets.py``;
|
||||
the plugin loader registers them via ``ctx.register_tool()``.
|
||||
|
||||
The ``apify`` toolset is default-off (``_DEFAULT_OFF_TOOLSETS`` in
|
||||
``hermes_cli/tools_config.py``) and the ``APIFY_API_TOKEN`` setup UX is wired
|
||||
through ``hermes tools`` (``TOOL_CATEGORIES``) and ``OPTIONAL_ENV_VARS``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, Dict
|
||||
|
||||
from plugins.apify.tools import (
|
||||
_COLLECT_SCHEMA,
|
||||
_DISCOVER_SCHEMA,
|
||||
_START_SCHEMA,
|
||||
_check_token,
|
||||
_collect_handler,
|
||||
_discover_handler,
|
||||
_start_handler,
|
||||
)
|
||||
|
||||
|
||||
async def _collect_handler_str(args: Dict[str, Any], **_kw: Any) -> str:
|
||||
return json.dumps(await _collect_handler(args), default=str)
|
||||
|
||||
|
||||
def register(ctx) -> None:
|
||||
"""Register the Apify Actor tools. Called once by the plugin loader."""
|
||||
ctx.register_tool(
|
||||
name="apify_discover",
|
||||
toolset="apify",
|
||||
schema=_DISCOVER_SCHEMA,
|
||||
handler=lambda args, **kw: json.dumps(_discover_handler(args), default=str),
|
||||
check_fn=_check_token,
|
||||
requires_env=["APIFY_API_TOKEN"],
|
||||
emoji="🔍",
|
||||
)
|
||||
ctx.register_tool(
|
||||
name="apify_start",
|
||||
toolset="apify",
|
||||
schema=_START_SCHEMA,
|
||||
handler=lambda args, **kw: json.dumps(_start_handler(args), default=str),
|
||||
check_fn=_check_token,
|
||||
requires_env=["APIFY_API_TOKEN"],
|
||||
emoji="▶️",
|
||||
)
|
||||
ctx.register_tool(
|
||||
name="apify_collect",
|
||||
toolset="apify",
|
||||
schema=_COLLECT_SCHEMA,
|
||||
handler=_collect_handler_str,
|
||||
check_fn=_check_token,
|
||||
requires_env=["APIFY_API_TOKEN"],
|
||||
is_async=True,
|
||||
emoji="📦",
|
||||
)
|
||||
@ -1,14 +1,18 @@
|
||||
"""Shared Apify SDK client — lazy import, token validation, and cache.
|
||||
|
||||
Extracted so both the web-search provider (plugins/web/apify/provider.py) and
|
||||
the Actor execution tools (tools/apify_tool.py) can import the client without
|
||||
depending on each other.
|
||||
Used by the Apify Actor execution tools (plugins/apify/tools.py). The
|
||||
``apify-client`` SDK is installed on demand via ``tools.lazy_deps`` so the
|
||||
dependency is only pulled when the user actually enables the plugin and runs
|
||||
an Actor.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any, Optional
|
||||
|
||||
# Sent with every request so Apify can attribute traffic to this integration.
|
||||
_HERMES_HEADERS = {"x-apify-integration-platform": "hermes-agent"}
|
||||
|
||||
_CLIENT_CLS: Optional[type] = None
|
||||
_CLIENT: Optional[Any] = None
|
||||
_CLIENT_CONFIG: Optional[Any] = None
|
||||
@ -49,7 +53,7 @@ def get_apify_client() -> Any:
|
||||
client_config = ("direct", api_token)
|
||||
if _CLIENT is not None and _CLIENT_CONFIG == client_config:
|
||||
return _CLIENT
|
||||
_CLIENT = _load_client_cls()(token=api_token)
|
||||
_CLIENT = _load_client_cls()(token=api_token, headers=_HERMES_HEADERS)
|
||||
_CLIENT_CONFIG = client_config
|
||||
return _CLIENT
|
||||
|
||||
9
plugins/apify/plugin.yaml
Normal file
9
plugins/apify/plugin.yaml
Normal file
@ -0,0 +1,9 @@
|
||||
name: apify
|
||||
version: 1.0.0
|
||||
description: "Apify Actor execution — 3 tools (discover, start, collect) for running any of 20,000+ Actors from the Apify Store (web automation, data extraction, social media, maps, e-commerce). Gated on APIFY_API_TOKEN. Toolset is default-off; enable via `hermes tools` → Apify Actors."
|
||||
author: JanHranicky
|
||||
kind: backend
|
||||
provides_tools:
|
||||
- apify_discover
|
||||
- apify_start
|
||||
- apify_collect
|
||||
@ -1,4 +1,9 @@
|
||||
"""Apify Actor execution tools — discover, start, collect."""
|
||||
"""Apify Actor execution tools — discover, start, collect.
|
||||
|
||||
Handlers and schemas for the three Apify tools. Registration happens in
|
||||
``plugins/apify/__init__.py`` via ``ctx.register_tool()`` (the plugin API),
|
||||
not via direct ``registry.register()`` calls.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
@ -6,8 +11,6 @@ import json
|
||||
import logging
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from tools.registry import registry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_TERMINAL_STATUSES = {"SUCCEEDED", "FAILED", "ABORTED", "TIMED-OUT"}
|
||||
@ -25,12 +28,12 @@ def _attr(obj: Any, key: str, default: Any = None) -> Any:
|
||||
|
||||
|
||||
def _get_client() -> Any:
|
||||
from tools.apify_client import get_apify_client
|
||||
from plugins.apify.client import get_apify_client
|
||||
return get_apify_client()
|
||||
|
||||
|
||||
def _check_token() -> bool:
|
||||
from tools.apify_client import check_apify_api_key
|
||||
from plugins.apify.client import check_apify_api_key
|
||||
return check_apify_api_key()
|
||||
|
||||
|
||||
@ -385,36 +388,3 @@ _COLLECT_SCHEMA: Dict[str, Any] = {
|
||||
"required": ["runs"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Registry
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
registry.register(
|
||||
name="apify_discover",
|
||||
toolset="apify",
|
||||
schema=_DISCOVER_SCHEMA,
|
||||
handler=lambda args, **kw: _discover_handler(args),
|
||||
check_fn=_check_token,
|
||||
emoji="🔍",
|
||||
)
|
||||
|
||||
registry.register(
|
||||
name="apify_start",
|
||||
toolset="apify",
|
||||
schema=_START_SCHEMA,
|
||||
handler=lambda args, **kw: _start_handler(args),
|
||||
check_fn=_check_token,
|
||||
emoji="▶️",
|
||||
)
|
||||
|
||||
registry.register(
|
||||
name="apify_collect",
|
||||
toolset="apify",
|
||||
schema=_COLLECT_SCHEMA,
|
||||
handler=lambda args, **kw: _collect_handler(args),
|
||||
check_fn=_check_token,
|
||||
is_async=True,
|
||||
emoji="📦",
|
||||
)
|
||||
@ -132,6 +132,7 @@ AUTHOR_MAP = {
|
||||
"tillfalko@gmail.com": "tillfalko",
|
||||
"hi@fesalfayed.com": "fesalfayed",
|
||||
"marek.les@seznam.cz": "maxcz79",
|
||||
"jan.hranicky@seznam.cz": "JanHranicky",
|
||||
# teknium (multiple emails)
|
||||
"teknium1@gmail.com": "teknium1",
|
||||
"kenyon1977@gmail.com": "kenyonxu",
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""Tests for tools/apify_tool.py — all mocked, no live Actor calls."""
|
||||
"""Tests for plugins/apify/tools.py — all mocked, no live Actor calls."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
@ -21,7 +21,7 @@ def mock_client(monkeypatch):
|
||||
"""
|
||||
client = MagicMock()
|
||||
monkeypatch.setattr(
|
||||
"tools.apify_tool._get_client",
|
||||
"plugins.apify.tools._get_client",
|
||||
lambda: client,
|
||||
)
|
||||
return client
|
||||
@ -53,7 +53,7 @@ class TestDiscoverStoreSearch:
|
||||
list_result.items = [actor_mock]
|
||||
mock_client.store.return_value.list.return_value = list_result
|
||||
|
||||
from tools.apify_tool import _discover_handler
|
||||
from plugins.apify.tools import _discover_handler
|
||||
result = _discover_handler({"query": "instagram scraper"})
|
||||
|
||||
assert "actors" in result
|
||||
@ -81,7 +81,7 @@ class TestDiscoverStoreSearch:
|
||||
list_result.items = [actor_mock]
|
||||
mock_client.store.return_value.list.return_value = list_result
|
||||
|
||||
from tools.apify_tool import _discover_handler
|
||||
from plugins.apify.tools import _discover_handler
|
||||
result = _discover_handler({"query": "test"})
|
||||
|
||||
assert len(result["actors"][0]["description"]) == 200
|
||||
@ -89,7 +89,7 @@ class TestDiscoverStoreSearch:
|
||||
def test_store_search_api_error_returns_error_dict(self, mock_client):
|
||||
mock_client.store.return_value.list.side_effect = RuntimeError("API error")
|
||||
|
||||
from tools.apify_tool import _discover_handler
|
||||
from plugins.apify.tools import _discover_handler
|
||||
result = _discover_handler({"query": "test"})
|
||||
|
||||
assert "error" in result
|
||||
@ -125,7 +125,7 @@ class TestDiscoverActorSchema:
|
||||
schema = {"type": "object", "properties": {"query": {"type": "string"}}}
|
||||
self._setup_build_mock(mock_client, input_schema=schema, readme="# README content")
|
||||
|
||||
from tools.apify_tool import _discover_handler
|
||||
from plugins.apify.tools import _discover_handler
|
||||
result = _discover_handler({"actor_id": "apify~google-search-scraper"})
|
||||
|
||||
assert result["actor_id"] == "apify~google-search-scraper"
|
||||
@ -141,7 +141,7 @@ class TestDiscoverActorSchema:
|
||||
def test_readme_truncated_to_3000_chars(self, mock_client):
|
||||
self._setup_build_mock(mock_client, readme="R" * 4000)
|
||||
|
||||
from tools.apify_tool import _discover_handler
|
||||
from plugins.apify.tools import _discover_handler
|
||||
result = _discover_handler({"actor_id": "apify~google-search-scraper"})
|
||||
|
||||
assert len(result["readme"]) == 3000
|
||||
@ -152,7 +152,7 @@ class TestDiscoverActorSchema:
|
||||
build_detail.inputSchema = '{"type":"object"}'
|
||||
build_detail.actorDefinition.input = None
|
||||
|
||||
from tools.apify_tool import _discover_handler
|
||||
from plugins.apify.tools import _discover_handler
|
||||
result = _discover_handler({"actor_id": "apify~google-search-scraper"})
|
||||
|
||||
assert result["input_schema"] == '{"type":"object"}'
|
||||
@ -160,7 +160,7 @@ class TestDiscoverActorSchema:
|
||||
def test_actor_not_found_returns_error(self, mock_client):
|
||||
mock_client.actor.return_value.get.return_value = None
|
||||
|
||||
from tools.apify_tool import _discover_handler
|
||||
from plugins.apify.tools import _discover_handler
|
||||
result = _discover_handler({"actor_id": "apify~nonexistent"})
|
||||
|
||||
assert "error" in result
|
||||
@ -173,20 +173,20 @@ class TestDiscoverActorSchema:
|
||||
|
||||
class TestDiscoverValidation:
|
||||
def test_missing_both_params_returns_error(self, mock_client):
|
||||
from tools.apify_tool import _discover_handler
|
||||
from plugins.apify.tools import _discover_handler
|
||||
result = _discover_handler({})
|
||||
assert "error" in result
|
||||
assert "query" in result["error"]
|
||||
assert "actor_id" in result["error"]
|
||||
|
||||
def test_empty_string_params_treated_as_missing(self, mock_client):
|
||||
from tools.apify_tool import _discover_handler
|
||||
from plugins.apify.tools import _discover_handler
|
||||
result = _discover_handler({"query": " ", "actor_id": ""})
|
||||
assert "error" in result
|
||||
|
||||
def test_interrupted_returns_error(self, mock_client, monkeypatch):
|
||||
monkeypatch.setattr("tools.interrupt.is_interrupted", lambda: True)
|
||||
from tools.apify_tool import _discover_handler
|
||||
from plugins.apify.tools import _discover_handler
|
||||
result = _discover_handler({"query": "test"})
|
||||
assert result == {"error": "Interrupted"}
|
||||
mock_client.store.assert_not_called()
|
||||
@ -207,7 +207,7 @@ class TestStart:
|
||||
def test_single_run_returns_run_ref(self, mock_client):
|
||||
mock_client.actor.return_value.start.return_value = self._make_run_mock()
|
||||
|
||||
from tools.apify_tool import _start_handler
|
||||
from plugins.apify.tools import _start_handler
|
||||
result = _start_handler({
|
||||
"runs": [{"actor_id": "apify~test", "input": {"key": "val"}}]
|
||||
})
|
||||
@ -226,7 +226,7 @@ class TestStart:
|
||||
def test_label_included_when_provided(self, mock_client):
|
||||
mock_client.actor.return_value.start.return_value = self._make_run_mock()
|
||||
|
||||
from tools.apify_tool import _start_handler
|
||||
from plugins.apify.tools import _start_handler
|
||||
result = _start_handler({
|
||||
"runs": [{"actor_id": "apify~test", "input": {}, "label": "my-run"}]
|
||||
})
|
||||
@ -236,7 +236,7 @@ class TestStart:
|
||||
def test_label_absent_when_not_provided(self, mock_client):
|
||||
mock_client.actor.return_value.start.return_value = self._make_run_mock()
|
||||
|
||||
from tools.apify_tool import _start_handler
|
||||
from plugins.apify.tools import _start_handler
|
||||
result = _start_handler({
|
||||
"runs": [{"actor_id": "apify~test", "input": {}}]
|
||||
})
|
||||
@ -248,7 +248,7 @@ class TestStart:
|
||||
run2 = self._make_run_mock(run_id="r2", dataset_id="d2")
|
||||
mock_client.actor.return_value.start.side_effect = [run1, run2]
|
||||
|
||||
from tools.apify_tool import _start_handler
|
||||
from plugins.apify.tools import _start_handler
|
||||
result = _start_handler({
|
||||
"runs": [
|
||||
{"actor_id": "apify~actor-a", "input": {}},
|
||||
@ -263,7 +263,7 @@ class TestStart:
|
||||
def test_per_run_api_error_goes_to_errors(self, mock_client):
|
||||
mock_client.actor.return_value.start.side_effect = RuntimeError("not found")
|
||||
|
||||
from tools.apify_tool import _start_handler
|
||||
from plugins.apify.tools import _start_handler
|
||||
result = _start_handler({
|
||||
"runs": [{"actor_id": "apify~bad-actor", "input": {}}]
|
||||
})
|
||||
@ -275,7 +275,7 @@ class TestStart:
|
||||
def test_interrupted_returns_early(self, mock_client, monkeypatch):
|
||||
monkeypatch.setattr("tools.interrupt.is_interrupted", lambda: True)
|
||||
|
||||
from tools.apify_tool import _start_handler
|
||||
from plugins.apify.tools import _start_handler
|
||||
result = _start_handler({"runs": [{"actor_id": "apify~test", "input": {}}]})
|
||||
|
||||
assert result == {"error": "Interrupted"}
|
||||
@ -296,7 +296,7 @@ class TestStart:
|
||||
interrupted_after_first = iter([False, False, True])
|
||||
monkeypatch.setattr("tools.interrupt.is_interrupted", lambda: next(interrupted_after_first))
|
||||
|
||||
from tools.apify_tool import _start_handler
|
||||
from plugins.apify.tools import _start_handler
|
||||
result = _start_handler({
|
||||
"runs": [
|
||||
{"actor_id": "apify~actor-a", "input": {}},
|
||||
@ -309,7 +309,7 @@ class TestStart:
|
||||
assert len(result["runs"]) == 1
|
||||
|
||||
def test_batch_over_limit_returns_error(self, mock_client):
|
||||
from tools.apify_tool import _start_handler, _MAX_BATCH_RUNS
|
||||
from plugins.apify.tools import _start_handler, _MAX_BATCH_RUNS
|
||||
oversized = [{"actor_id": f"apify~actor-{i}", "input": {}} for i in range(_MAX_BATCH_RUNS + 1)]
|
||||
result = _start_handler({"runs": oversized})
|
||||
assert "error" in result
|
||||
@ -328,7 +328,7 @@ class TestCollectNonTerminal:
|
||||
run_info.status = "RUNNING"
|
||||
mock_client.run.return_value.get.return_value = run_info
|
||||
|
||||
from tools.apify_tool import _collect_handler
|
||||
from plugins.apify.tools import _collect_handler
|
||||
result = await _collect_handler({
|
||||
"runs": [{"run_id": "r1", "actor_id": "apify~test", "dataset_id": "d1"}]
|
||||
})
|
||||
@ -346,7 +346,7 @@ class TestCollectNonTerminal:
|
||||
run_info.status = "QUEUED"
|
||||
mock_client.run.return_value.get.return_value = run_info
|
||||
|
||||
from tools.apify_tool import _collect_handler
|
||||
from plugins.apify.tools import _collect_handler
|
||||
result = await _collect_handler({
|
||||
"runs": [{"run_id": "r1", "actor_id": "apify~test", "dataset_id": "d1"}]
|
||||
})
|
||||
@ -360,7 +360,7 @@ class TestCollectNonTerminal:
|
||||
run_info.status = "FAILED"
|
||||
mock_client.run.return_value.get.return_value = run_info
|
||||
|
||||
from tools.apify_tool import _collect_handler
|
||||
from plugins.apify.tools import _collect_handler
|
||||
result = await _collect_handler({
|
||||
"runs": [{"run_id": "r1", "actor_id": "apify~test", "dataset_id": "d1"}]
|
||||
})
|
||||
@ -375,7 +375,7 @@ class TestCollectNonTerminal:
|
||||
run_info.status = "ABORTED"
|
||||
mock_client.run.return_value.get.return_value = run_info
|
||||
|
||||
from tools.apify_tool import _collect_handler
|
||||
from plugins.apify.tools import _collect_handler
|
||||
result = await _collect_handler({
|
||||
"runs": [{"run_id": "r1", "actor_id": "apify~test", "dataset_id": "d1"}]
|
||||
})
|
||||
@ -386,7 +386,7 @@ class TestCollectNonTerminal:
|
||||
async def test_run_not_found_goes_to_errors(self, mock_client):
|
||||
mock_client.run.return_value.get.return_value = None
|
||||
|
||||
from tools.apify_tool import _collect_handler
|
||||
from plugins.apify.tools import _collect_handler
|
||||
result = await _collect_handler({
|
||||
"runs": [{"run_id": "r1", "actor_id": "apify~test", "dataset_id": "d1"}]
|
||||
})
|
||||
@ -399,7 +399,7 @@ class TestCollectNonTerminal:
|
||||
run_info.status = "RUNNING"
|
||||
mock_client.run.return_value.get.return_value = run_info
|
||||
|
||||
from tools.apify_tool import _collect_handler
|
||||
from plugins.apify.tools import _collect_handler
|
||||
result = await _collect_handler({
|
||||
"runs": [{"run_id": "r1", "actor_id": "apify~test", "dataset_id": "d1", "label": "instagram"}]
|
||||
})
|
||||
@ -423,7 +423,7 @@ class TestCollectSucceeded:
|
||||
dataset_result.items = items
|
||||
mock_client.dataset.return_value.list_items.return_value = dataset_result
|
||||
|
||||
from tools.apify_tool import _collect_handler
|
||||
from plugins.apify.tools import _collect_handler
|
||||
result = await _collect_handler({
|
||||
"runs": [{"run_id": "r1", "actor_id": "apify~test", "dataset_id": "d1"}]
|
||||
})
|
||||
@ -436,7 +436,7 @@ class TestCollectSucceeded:
|
||||
assert "<<<EXTERNAL_UNTRUSTED_CONTENT>>>" in c["data"]
|
||||
assert "<<<END_EXTERNAL_UNTRUSTED_CONTENT>>>" in c["data"]
|
||||
assert "Result 1" in c["data"]
|
||||
from tools.apify_tool import _COLLECT_DEFAULT_LIMIT
|
||||
from plugins.apify.tools import _COLLECT_DEFAULT_LIMIT
|
||||
mock_client.dataset.return_value.list_items.assert_called_once_with(limit=_COLLECT_DEFAULT_LIMIT)
|
||||
mock_client.dataset.assert_called_once_with("d1")
|
||||
|
||||
@ -452,7 +452,7 @@ class TestCollectSucceeded:
|
||||
dataset_result.items = items
|
||||
mock_client.dataset.return_value.list_items.return_value = dataset_result
|
||||
|
||||
from tools.apify_tool import _collect_handler
|
||||
from plugins.apify.tools import _collect_handler
|
||||
result = await _collect_handler({
|
||||
"runs": [{"run_id": "r1", "actor_id": "apify~test", "dataset_id": "d1"}]
|
||||
})
|
||||
@ -472,7 +472,7 @@ class TestCollectSucceeded:
|
||||
mock_client.run.return_value.get.return_value = run_info
|
||||
mock_client.dataset.return_value.list_items.return_value = MagicMock(items=[])
|
||||
|
||||
from tools.apify_tool import _collect_handler
|
||||
from plugins.apify.tools import _collect_handler
|
||||
result = await _collect_handler({
|
||||
"runs": [{"run_id": "r1", "actor_id": "apify~test", "dataset_id": "d1"}]
|
||||
})
|
||||
@ -482,7 +482,7 @@ class TestCollectSucceeded:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_may_have_more_set_when_result_count_equals_limit(self, mock_client):
|
||||
from tools.apify_tool import _collect_handler, _COLLECT_DEFAULT_LIMIT
|
||||
from plugins.apify.tools import _collect_handler, _COLLECT_DEFAULT_LIMIT
|
||||
run_info = MagicMock()
|
||||
run_info.status = "SUCCEEDED"
|
||||
mock_client.run.return_value.get.return_value = run_info
|
||||
@ -502,7 +502,7 @@ class TestCollectSucceeded:
|
||||
mock_client.run.return_value.get.return_value = run_info
|
||||
mock_client.dataset.return_value.list_items.return_value = MagicMock(items=[{"i": 0}])
|
||||
|
||||
from tools.apify_tool import _collect_handler
|
||||
from plugins.apify.tools import _collect_handler
|
||||
result = await _collect_handler({
|
||||
"runs": [{"run_id": "r1", "actor_id": "apify~test", "dataset_id": "d1"}]
|
||||
})
|
||||
@ -516,7 +516,7 @@ class TestCollectSucceeded:
|
||||
mock_client.run.return_value.get.return_value = run_info
|
||||
mock_client.dataset.return_value.list_items.return_value = MagicMock(items=[])
|
||||
|
||||
from tools.apify_tool import _collect_handler
|
||||
from plugins.apify.tools import _collect_handler
|
||||
await _collect_handler({
|
||||
"runs": [{"run_id": "r1", "actor_id": "apify~test", "dataset_id": "d1"}],
|
||||
"limit": 500,
|
||||
@ -548,7 +548,7 @@ class TestCollectMixed:
|
||||
mock_client.run.side_effect = _run_get_side_effect
|
||||
mock_client.dataset.return_value.list_items.return_value = MagicMock(items=[{"result": 1}])
|
||||
|
||||
from tools.apify_tool import _collect_handler
|
||||
from plugins.apify.tools import _collect_handler
|
||||
result = await _collect_handler({
|
||||
"runs": [
|
||||
{"run_id": "r1", "actor_id": "apify~a", "dataset_id": "d1"},
|
||||
@ -566,7 +566,7 @@ class TestCollectMixed:
|
||||
async def test_interrupted_returns_early(self, mock_client, monkeypatch):
|
||||
monkeypatch.setattr("tools.interrupt.is_interrupted", lambda: True)
|
||||
|
||||
from tools.apify_tool import _collect_handler
|
||||
from plugins.apify.tools import _collect_handler
|
||||
result = await _collect_handler({
|
||||
"runs": [{"run_id": "r1", "actor_id": "apify~test", "dataset_id": "d1"}]
|
||||
})
|
||||
@ -578,7 +578,7 @@ class TestCollectMixed:
|
||||
async def test_api_exception_goes_to_errors(self, mock_client):
|
||||
mock_client.run.return_value.get.side_effect = RuntimeError("API error")
|
||||
|
||||
from tools.apify_tool import _collect_handler
|
||||
from plugins.apify.tools import _collect_handler
|
||||
result = await _collect_handler({
|
||||
"runs": [{"run_id": "r1", "actor_id": "apify~test", "dataset_id": "d1"}]
|
||||
})
|
||||
@ -597,7 +597,7 @@ class TestCollectFullWorkflow:
|
||||
run2 = MagicMock(id="r2", default_dataset_id="d2", status="QUEUED")
|
||||
mock_client.actor.return_value.start.side_effect = [run1, run2]
|
||||
|
||||
from tools.apify_tool import _start_handler, _collect_handler
|
||||
from plugins.apify.tools import _start_handler, _collect_handler
|
||||
|
||||
start_result = _start_handler({
|
||||
"runs": [
|
||||
@ -68,8 +68,6 @@ _HERMES_CORE_TOOLS = [
|
||||
"kanban_complete", "kanban_block", "kanban_heartbeat",
|
||||
"kanban_comment", "kanban_create", "kanban_link",
|
||||
"kanban_unblock",
|
||||
# Apify Actor execution (gated on APIFY_API_TOKEN via check_fn)
|
||||
"apify_discover", "apify_start", "apify_collect",
|
||||
# Computer use (macOS, gated on cua-driver being installed via check_fn)
|
||||
"computer_use",
|
||||
]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user