apify-hermes-agent-plugin 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apify_hermes_agent_plugin-0.1.0/CHANGELOG.md +15 -0
- apify_hermes_agent_plugin-0.1.0/LICENSE +21 -0
- apify_hermes_agent_plugin-0.1.0/MANIFEST.in +2 -0
- apify_hermes_agent_plugin-0.1.0/PKG-INFO +63 -0
- apify_hermes_agent_plugin-0.1.0/README.md +29 -0
- apify_hermes_agent_plugin-0.1.0/pyproject.toml +121 -0
- apify_hermes_agent_plugin-0.1.0/setup.cfg +4 -0
- apify_hermes_agent_plugin-0.1.0/src/apify_hermes_agent_plugin/__init__.py +45 -0
- apify_hermes_agent_plugin-0.1.0/src/apify_hermes_agent_plugin/cli.py +86 -0
- apify_hermes_agent_plugin-0.1.0/src/apify_hermes_agent_plugin/client.py +45 -0
- apify_hermes_agent_plugin-0.1.0/src/apify_hermes_agent_plugin/tools.py +431 -0
- apify_hermes_agent_plugin-0.1.0/src/apify_hermes_agent_plugin.egg-info/PKG-INFO +63 -0
- apify_hermes_agent_plugin-0.1.0/src/apify_hermes_agent_plugin.egg-info/SOURCES.txt +15 -0
- apify_hermes_agent_plugin-0.1.0/src/apify_hermes_agent_plugin.egg-info/dependency_links.txt +1 -0
- apify_hermes_agent_plugin-0.1.0/src/apify_hermes_agent_plugin.egg-info/entry_points.txt +2 -0
- apify_hermes_agent_plugin-0.1.0/src/apify_hermes_agent_plugin.egg-info/requires.txt +8 -0
- apify_hermes_agent_plugin-0.1.0/src/apify_hermes_agent_plugin.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-08-11
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Initial release.
|
|
12
|
+
- `apify_discover` tool — search the Apify Store by keyword, or fetch an Actor's input schema and README by `actor_id`.
|
|
13
|
+
- `apify_start` tool — fire-and-forget batch Actor runs (up to 10 per call).
|
|
14
|
+
- `apify_collect` tool — poll run statuses and return completed dataset results.
|
|
15
|
+
- `hermes apify-setup` CLI command — prompt for (or accept via `--token`) the `APIFY_API_TOKEN` and enable the Apify toolset.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Apify
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: apify-hermes-agent-plugin
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Hermes Agent plugin for Apify, the world's largest marketplace for AI tools — discover, run, and collect results from Actors, all from natural language
|
|
5
|
+
Author-email: "Apify Technologies s.r.o." <support@apify.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/apify/apify-hermes-agent-plugin
|
|
8
|
+
Project-URL: Source Code, https://github.com/apify/apify-hermes-agent-plugin
|
|
9
|
+
Project-URL: Issue Tracker, https://github.com/apify/apify-hermes-agent-plugin/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/apify/apify-hermes-agent-plugin/blob/main/CHANGELOG.md
|
|
11
|
+
Project-URL: Hermes Agent, https://hermes-agent.nousresearch.com
|
|
12
|
+
Keywords: apify,hermes,hermes-agent,actor,plugin,ai-tools,automation
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: hermes-agent>=0.15.1
|
|
27
|
+
Requires-Dist: apify-client<4,>=3.0.1
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest==9.0.2; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-asyncio==1.3.0; extra == "dev"
|
|
31
|
+
Requires-Dist: ruff<0.17,>=0.15.20; extra == "dev"
|
|
32
|
+
Requires-Dist: ty<0.1,>=0.0.65; extra == "dev"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# apify-hermes-agent-plugin
|
|
36
|
+
|
|
37
|
+
Hermes Agent plugin for [Apify](https://apify.com), the world's largest marketplace for AI tools — discover
|
|
38
|
+
Actors in the Apify Store, start runs, and collect structured results, all from natural language, via
|
|
39
|
+
[Hermes Agent](https://hermes-agent.nousresearch.com).
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
pip install apify-hermes-agent-plugin
|
|
44
|
+
hermes plugins enable apify
|
|
45
|
+
|
|
46
|
+
Then run:
|
|
47
|
+
|
|
48
|
+
hermes apify-setup
|
|
49
|
+
|
|
50
|
+
This prompts for your `APIFY_API_TOKEN` (get one at https://apify.com/account/integrations),
|
|
51
|
+
saves it, and enables the Apify Actors toolset for the CLI. Pass `--token <token>` to skip the
|
|
52
|
+
prompt.
|
|
53
|
+
|
|
54
|
+
## Tools
|
|
55
|
+
|
|
56
|
+
- `apify_discover` — search the Apify Store by keyword, or fetch an Actor's input schema + README by `actor_id`.
|
|
57
|
+
- `apify_start` — fire-and-forget batch Actor starts (up to 10 per call).
|
|
58
|
+
- `apify_collect` — poll run statuses and return completed dataset results.
|
|
59
|
+
|
|
60
|
+
## Development
|
|
61
|
+
|
|
62
|
+
pip install -e ".[dev]"
|
|
63
|
+
pytest
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# apify-hermes-agent-plugin
|
|
2
|
+
|
|
3
|
+
Hermes Agent plugin for [Apify](https://apify.com), the world's largest marketplace for AI tools — discover
|
|
4
|
+
Actors in the Apify Store, start runs, and collect structured results, all from natural language, via
|
|
5
|
+
[Hermes Agent](https://hermes-agent.nousresearch.com).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
pip install apify-hermes-agent-plugin
|
|
10
|
+
hermes plugins enable apify
|
|
11
|
+
|
|
12
|
+
Then run:
|
|
13
|
+
|
|
14
|
+
hermes apify-setup
|
|
15
|
+
|
|
16
|
+
This prompts for your `APIFY_API_TOKEN` (get one at https://apify.com/account/integrations),
|
|
17
|
+
saves it, and enables the Apify Actors toolset for the CLI. Pass `--token <token>` to skip the
|
|
18
|
+
prompt.
|
|
19
|
+
|
|
20
|
+
## Tools
|
|
21
|
+
|
|
22
|
+
- `apify_discover` — search the Apify Store by keyword, or fetch an Actor's input schema + README by `actor_id`.
|
|
23
|
+
- `apify_start` — fire-and-forget batch Actor starts (up to 10 per call).
|
|
24
|
+
- `apify_collect` — poll run statuses and return completed dataset results.
|
|
25
|
+
|
|
26
|
+
## Development
|
|
27
|
+
|
|
28
|
+
pip install -e ".[dev]"
|
|
29
|
+
pytest
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "apify-hermes-agent-plugin"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Hermes Agent plugin for Apify, the world's largest marketplace for AI tools — discover, run, and collect results from Actors, all from natural language"
|
|
9
|
+
authors = [{ name = "Apify Technologies s.r.o.", email = "support@apify.com" }]
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
license = { text = "MIT" }
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
23
|
+
"Topic :: Software Development :: Libraries",
|
|
24
|
+
]
|
|
25
|
+
keywords = ["apify", "hermes", "hermes-agent", "actor", "plugin", "ai-tools", "automation"]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"hermes-agent>=0.15.1",
|
|
28
|
+
"apify-client>=3.0.1,<4",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = ["pytest==9.0.2", "pytest-asyncio==1.3.0", "ruff>=0.15.20,<0.17", "ty>=0.0.65,<0.1"]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
"Homepage" = "https://github.com/apify/apify-hermes-agent-plugin"
|
|
36
|
+
"Source Code" = "https://github.com/apify/apify-hermes-agent-plugin"
|
|
37
|
+
"Issue Tracker" = "https://github.com/apify/apify-hermes-agent-plugin/issues"
|
|
38
|
+
"Changelog" = "https://github.com/apify/apify-hermes-agent-plugin/blob/main/CHANGELOG.md"
|
|
39
|
+
"Hermes Agent" = "https://hermes-agent.nousresearch.com"
|
|
40
|
+
|
|
41
|
+
[project.entry-points."hermes_agent.plugins"]
|
|
42
|
+
# NOTE: must point at the bare module, not "apify_hermes_agent_plugin:register".
|
|
43
|
+
# hermes-agent's loader (hermes_cli/plugins.py::_load_entrypoint_module) calls
|
|
44
|
+
# ep.load() and then does getattr(result, "register") — it expects a module.
|
|
45
|
+
# Per importlib.metadata semantics, "module:attr" entry points make ep.load()
|
|
46
|
+
# return the attr itself (the register function), not the module, so
|
|
47
|
+
# getattr(register_fn, "register") finds nothing and the plugin silently
|
|
48
|
+
# fails to load ("no register() function"). This diverges from hermes-agent's
|
|
49
|
+
# own plugin docs example (which shows "my_plugin:register") — see
|
|
50
|
+
# https://hermes-agent.nousresearch.com/docs/user-guide/features/plugins.
|
|
51
|
+
apify = "apify_hermes_agent_plugin"
|
|
52
|
+
|
|
53
|
+
[tool.setuptools.packages.find]
|
|
54
|
+
where = ["src"]
|
|
55
|
+
|
|
56
|
+
[tool.pytest.ini_options]
|
|
57
|
+
testpaths = ["tests"]
|
|
58
|
+
asyncio_mode = "strict"
|
|
59
|
+
|
|
60
|
+
[tool.ruff]
|
|
61
|
+
line-length = 120
|
|
62
|
+
include = ["src/**/*.py", "tests/**/*.py"]
|
|
63
|
+
|
|
64
|
+
[tool.ruff.format]
|
|
65
|
+
quote-style = "single"
|
|
66
|
+
indent-style = "space"
|
|
67
|
+
|
|
68
|
+
[tool.ruff.lint.flake8-quotes]
|
|
69
|
+
docstring-quotes = "double"
|
|
70
|
+
inline-quotes = "single"
|
|
71
|
+
|
|
72
|
+
[tool.ruff.lint]
|
|
73
|
+
select = ["ALL"]
|
|
74
|
+
ignore = [
|
|
75
|
+
"ANN401", # `Any` is disallowed — this plugin's whole surface is dynamically-typed SDK
|
|
76
|
+
# responses (apify-client returns Pydantic models OR dicts depending on call site)
|
|
77
|
+
# and JSON-schema-validated tool args; `Any` here is accurate, not lazy.
|
|
78
|
+
"BLE001", # Do not catch blind exception — every handler must turn arbitrary SDK/network
|
|
79
|
+
# failures into a JSON error object for the LLM, never let one crash the tool call.
|
|
80
|
+
"COM812", # Conflicts with the formatter.
|
|
81
|
+
"CPY001", # Missing copyright notice — this is an MIT-licensed package (see LICENSE at the
|
|
82
|
+
# repo root); per-file copyright headers aren't this project's convention.
|
|
83
|
+
"D100", # Missing docstring in public module.
|
|
84
|
+
"D104", # Missing docstring in public package.
|
|
85
|
+
"D107", # Missing docstring in `__init__`.
|
|
86
|
+
"D203", # One blank line required before class docstring.
|
|
87
|
+
"D213", # Multi-line docstring summary should start at the second line.
|
|
88
|
+
"PLC0415", # Import should be at top-level — deliberate: `tools.py`/`cli.py` re-import
|
|
89
|
+
# `tools.interrupt.is_interrupted` and hermes_cli internals inside each function
|
|
90
|
+
# so tests can `monkeypatch.setattr("tools.interrupt.is_interrupted", ...)` and
|
|
91
|
+
# have it take effect. A module-level import would bind the original function
|
|
92
|
+
# once at import time and monkeypatching the source module afterwards would not
|
|
93
|
+
# reach it — see tests/test_tools.py's `not_interrupted` fixture.
|
|
94
|
+
"PLR0913", # Too many arguments in function definition.
|
|
95
|
+
"PLW0603", # Using `global` — `src/apify_hermes_agent_plugin/client.py`'s two module-level
|
|
96
|
+
# variables are a deliberate single-client memoization cache; a wrapper class for
|
|
97
|
+
# two variables would be more ceremony than the pattern it replaces.
|
|
98
|
+
"EM101", # Exception must not use a string literal — validation errors are one-off, not reused.
|
|
99
|
+
"EM102", # Exception must not use an f-string literal — same reason.
|
|
100
|
+
"TRY003", # Avoid specifying long messages outside the exception class — same reason.
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
[tool.ruff.lint.per-file-ignores]
|
|
104
|
+
"tests/*" = [
|
|
105
|
+
"ANN", # Type annotations on test helpers/mocks add noise, not safety.
|
|
106
|
+
"ARG001", # Mocks commonly do not use all arguments.
|
|
107
|
+
"ARG002", # Same — pytest fixtures (e.g. an autouse-style mock_client) are commonly
|
|
108
|
+
# accepted by a test method for their side effect and never referenced in the body.
|
|
109
|
+
"ARG005", # Same — monkeypatch stub lambdas commonly ignore their arguments.
|
|
110
|
+
"D", # Missing docstrings.
|
|
111
|
+
"INP001", # Implicit namespace package — tests/ deliberately has no __init__.py (a pytest
|
|
112
|
+
# rootdir/import-mode choice), so this rule will always fire here; it's a
|
|
113
|
+
# permanent ignore, not pending work.
|
|
114
|
+
"PLR2004", # Magic value used in comparison.
|
|
115
|
+
"S101", # Use of assert detected.
|
|
116
|
+
"S105", # Possible hardcoded password — these are fake test tokens.
|
|
117
|
+
"S106", # Possible hardcoded password assigned to argument — same reason.
|
|
118
|
+
]
|
|
119
|
+
"src/apify_hermes_agent_plugin/cli.py" = [
|
|
120
|
+
"T201", # `print` found — this file *is* the CLI's terminal UI; printing is the point.
|
|
121
|
+
]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""apify-hermes-agent-plugin — Apify Actor execution tools for Hermes Agent."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from apify_hermes_agent_plugin.cli import apify_setup_command, register_cli
|
|
8
|
+
from apify_hermes_agent_plugin.tools import (
|
|
9
|
+
_COLLECT_SCHEMA,
|
|
10
|
+
_DISCOVER_SCHEMA,
|
|
11
|
+
_START_SCHEMA,
|
|
12
|
+
_check_token,
|
|
13
|
+
collect_handler_str,
|
|
14
|
+
discover_handler_str,
|
|
15
|
+
start_handler_str,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
_TOOLS = (
|
|
19
|
+
('apify_discover', _DISCOVER_SCHEMA, discover_handler_str, '🔍', False),
|
|
20
|
+
('apify_start', _START_SCHEMA, start_handler_str, '▶️', False),
|
|
21
|
+
('apify_collect', _COLLECT_SCHEMA, collect_handler_str, '📦', True),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def register(ctx: Any) -> None:
|
|
26
|
+
"""Register the three Apify Actor tools. Called once by the Hermes plugin loader."""
|
|
27
|
+
for name, schema, handler, emoji, is_async in _TOOLS:
|
|
28
|
+
ctx.register_tool(
|
|
29
|
+
name=name,
|
|
30
|
+
toolset='apify',
|
|
31
|
+
schema=schema,
|
|
32
|
+
handler=handler,
|
|
33
|
+
check_fn=_check_token,
|
|
34
|
+
requires_env=['APIFY_API_TOKEN'],
|
|
35
|
+
is_async=is_async,
|
|
36
|
+
emoji=emoji,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
ctx.register_cli_command(
|
|
40
|
+
name='apify-setup',
|
|
41
|
+
help='Set your Apify API token',
|
|
42
|
+
setup_fn=register_cli,
|
|
43
|
+
handler_fn=apify_setup_command,
|
|
44
|
+
description=('Prompt for (or accept via --token) your APIFY_API_TOKEN and save it to ~/.hermes/.env.'),
|
|
45
|
+
)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""``hermes apify-setup`` — prompt for and save the Apify API token."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import getpass
|
|
6
|
+
import sys
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
import argparse
|
|
11
|
+
|
|
12
|
+
_ENV_KEY = 'APIFY_API_TOKEN'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def register_cli(parser: argparse.ArgumentParser) -> None:
|
|
16
|
+
"""Wire up ``hermes apify-setup`` argument parsing.
|
|
17
|
+
|
|
18
|
+
Called by the Hermes plugin loader with the parser for this subcommand.
|
|
19
|
+
"""
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
'--token',
|
|
22
|
+
default=None,
|
|
23
|
+
help=("Apify API token (skips the interactive prompt). If omitted, you'll be prompted securely."),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def apify_setup_command(args: argparse.Namespace) -> int:
|
|
28
|
+
"""Prompt for (or accept via --token) the Apify API token and save it."""
|
|
29
|
+
token = getattr(args, 'token', None)
|
|
30
|
+
if token is not None:
|
|
31
|
+
token = token.strip()
|
|
32
|
+
else:
|
|
33
|
+
print('Set your Apify API token (get one at https://apify.com/account/integrations).')
|
|
34
|
+
token = getpass.getpass(f'{_ENV_KEY}: ').strip()
|
|
35
|
+
|
|
36
|
+
if not token:
|
|
37
|
+
print('No token provided — aborted.')
|
|
38
|
+
sys.exit(1)
|
|
39
|
+
|
|
40
|
+
from hermes_cli.config import get_env_path, save_env_value
|
|
41
|
+
|
|
42
|
+
save_env_value(_ENV_KEY, token)
|
|
43
|
+
print(f'Saved {_ENV_KEY} to {get_env_path()}')
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
_enable_apify_toolset_for_cli()
|
|
47
|
+
print('Enabled the Apify Actors toolset for the CLI.')
|
|
48
|
+
except Exception as exc:
|
|
49
|
+
print(f'Could not auto-enable the Apify Actors toolset ({exc}).')
|
|
50
|
+
print('Run `hermes tools` and enable it manually.')
|
|
51
|
+
|
|
52
|
+
return 0
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _enable_apify_toolset_for_cli() -> None:
|
|
56
|
+
"""Add "apify" to the set of toolsets enabled for the "cli" platform.
|
|
57
|
+
|
|
58
|
+
Reuses hermes-agent's own config read/merge/save logic (rather than
|
|
59
|
+
reimplementing it) since it has non-obvious invariants — reconciling
|
|
60
|
+
agent.disabled_toolsets, preserving MCP server entries, plugin-toolset
|
|
61
|
+
bookkeeping. These are private hermes_cli internals with no stability
|
|
62
|
+
guarantee; callers should treat failure here as non-fatal.
|
|
63
|
+
|
|
64
|
+
If the "cli" platform has no explicit toolset list yet, resolving and
|
|
65
|
+
saving it (via ``_get_platform_tools``) would expand the implicit
|
|
66
|
+
default composite (every default-on toolset, plus MCP servers) into a
|
|
67
|
+
frozen explicit snapshot — silently opting this user out of any future
|
|
68
|
+
default-on toolset that ships in a later hermes-agent release. Instead,
|
|
69
|
+
keep the composite referenced by name (the same "[hermes-cli, spotify]"
|
|
70
|
+
mixed-config shape ``_get_platform_tools`` already supports for a single
|
|
71
|
+
explicit opt-in alongside the defaults).
|
|
72
|
+
"""
|
|
73
|
+
from hermes_cli.config import load_config
|
|
74
|
+
from hermes_cli.tools_config import PLATFORMS, _get_platform_tools, _save_platform_tools
|
|
75
|
+
|
|
76
|
+
config = load_config()
|
|
77
|
+
existing = (config.get('platform_toolsets') or {}).get('cli')
|
|
78
|
+
|
|
79
|
+
if isinstance(existing, list):
|
|
80
|
+
enabled = _get_platform_tools(config, 'cli')
|
|
81
|
+
else:
|
|
82
|
+
default_ts = PLATFORMS.get('cli', {}).get('default_toolset', 'hermes-cli')
|
|
83
|
+
enabled = {default_ts}
|
|
84
|
+
|
|
85
|
+
enabled.add('apify')
|
|
86
|
+
_save_platform_tools(config, 'cli', enabled)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Apify SDK client — token validation and cache."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from apify_client import ApifyClient
|
|
9
|
+
|
|
10
|
+
# Sent with every request so Apify can attribute traffic to this integration.
|
|
11
|
+
_HERMES_HEADERS = {'x-apify-integration-platform': 'hermes-agent'}
|
|
12
|
+
|
|
13
|
+
_CLIENT: Any | None = None
|
|
14
|
+
_CLIENT_CONFIG: Any | None = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def check_apify_api_key() -> bool:
|
|
18
|
+
"""Return True when APIFY_API_TOKEN is configured."""
|
|
19
|
+
return bool(os.getenv('APIFY_API_TOKEN', '').strip())
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_apify_client() -> Any:
|
|
23
|
+
"""Return a cached ApifyClient built from APIFY_API_TOKEN.
|
|
24
|
+
|
|
25
|
+
Raises ValueError when the token is not set.
|
|
26
|
+
"""
|
|
27
|
+
global _CLIENT, _CLIENT_CONFIG
|
|
28
|
+
api_token = os.getenv('APIFY_API_TOKEN', '').strip()
|
|
29
|
+
if not api_token:
|
|
30
|
+
raise ValueError(
|
|
31
|
+
'Apify tools are not configured. Set APIFY_API_TOKEN (get one at https://apify.com/account/integrations).'
|
|
32
|
+
)
|
|
33
|
+
client_config = ('direct', api_token)
|
|
34
|
+
if _CLIENT is not None and client_config == _CLIENT_CONFIG:
|
|
35
|
+
return _CLIENT
|
|
36
|
+
_CLIENT = ApifyClient(token=api_token, headers=_HERMES_HEADERS)
|
|
37
|
+
_CLIENT_CONFIG = client_config
|
|
38
|
+
return _CLIENT
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _reset_client_for_tests() -> None:
|
|
42
|
+
"""Drop cached client so tests can re-instantiate cleanly."""
|
|
43
|
+
global _CLIENT, _CLIENT_CONFIG
|
|
44
|
+
_CLIENT = None
|
|
45
|
+
_CLIENT_CONFIG = None
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
"""Apify Actor execution tools — discover, start, collect."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
_TERMINAL_STATUSES = {'SUCCEEDED', 'FAILED', 'ABORTED', 'TIMED-OUT'}
|
|
13
|
+
_MAX_BATCH_RUNS = 10
|
|
14
|
+
_COLLECT_DEFAULT_LIMIT = 100
|
|
15
|
+
_MAX_CONTENT_CHARS = 50_000
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _attr(obj: Any, key: str, default: Any = None) -> Any:
|
|
19
|
+
"""Get attribute or dict key from SDK response objects (apify_client returns either)."""
|
|
20
|
+
if isinstance(obj, dict):
|
|
21
|
+
return obj.get(key, default)
|
|
22
|
+
if obj is not None and hasattr(obj, key):
|
|
23
|
+
return getattr(obj, key)
|
|
24
|
+
return default
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _get_client() -> Any:
|
|
28
|
+
from apify_hermes_agent_plugin.client import get_apify_client
|
|
29
|
+
|
|
30
|
+
return get_apify_client()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _check_token() -> bool:
|
|
34
|
+
from apify_hermes_agent_plugin.client import check_apify_api_key
|
|
35
|
+
|
|
36
|
+
return check_apify_api_key()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
# Handlers
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _discover_handler(args: dict[str, Any]) -> dict[str, Any]:
|
|
45
|
+
from tools.interrupt import is_interrupted
|
|
46
|
+
|
|
47
|
+
if is_interrupted():
|
|
48
|
+
return {'error': 'Interrupted'}
|
|
49
|
+
|
|
50
|
+
query = (_attr(args, 'query') or '').strip() or None
|
|
51
|
+
actor_id = (_attr(args, 'actor_id') or '').strip() or None
|
|
52
|
+
|
|
53
|
+
if not query and not actor_id:
|
|
54
|
+
return {
|
|
55
|
+
'error': (
|
|
56
|
+
"Provide exactly one of 'query' (to search the Apify Store) "
|
|
57
|
+
"or 'actor_id' (to fetch an Actor's input schema)."
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
client = _get_client()
|
|
62
|
+
if actor_id:
|
|
63
|
+
return _discover_actor(client, actor_id)
|
|
64
|
+
# This is a type-narrowing hint for `ty`, not a `-O`-surviving safety net — it's provably
|
|
65
|
+
# redundant given the `query`/`actor_id` guard above, so it's fine if stripped.
|
|
66
|
+
assert query is not None, 'guaranteed by the query/actor_id check above' # noqa: S101
|
|
67
|
+
return _discover_store(client, query)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _discover_actor(client: Any, actor_id: str) -> dict[str, Any]:
|
|
71
|
+
"""Fetch a single Actor's input schema and README by ID or username~actor-name."""
|
|
72
|
+
try:
|
|
73
|
+
actor_info = client.actor(actor_id).get()
|
|
74
|
+
if actor_info is None:
|
|
75
|
+
return {'error': (f"Actor '{actor_id}' not found. Check the ID format: username~actor-name.")}
|
|
76
|
+
|
|
77
|
+
input_schema: Any = None
|
|
78
|
+
readme: Any = None
|
|
79
|
+
|
|
80
|
+
build_detail = client.actor(actor_id).default_build().get()
|
|
81
|
+
if build_detail is not None:
|
|
82
|
+
# apify-client 3.x returns Pydantic models with snake_case attributes
|
|
83
|
+
# (e.g. Build.actor_definition, Build.input_schema) — the JSON's
|
|
84
|
+
# camelCase names only exist as (de)serialization aliases, not as
|
|
85
|
+
# real attributes, so _attr() must be called with the snake_case name.
|
|
86
|
+
actor_def = _attr(build_detail, 'actor_definition')
|
|
87
|
+
raw_schema = _attr(actor_def, 'input')
|
|
88
|
+
if raw_schema:
|
|
89
|
+
input_schema = json.dumps(raw_schema)
|
|
90
|
+
else:
|
|
91
|
+
fallback = _attr(build_detail, 'input_schema')
|
|
92
|
+
if fallback:
|
|
93
|
+
input_schema = str(fallback)
|
|
94
|
+
|
|
95
|
+
raw_readme = _attr(actor_def, 'readme') or _attr(build_detail, 'readme')
|
|
96
|
+
if raw_readme:
|
|
97
|
+
readme = str(raw_readme)[:3000]
|
|
98
|
+
|
|
99
|
+
username = _attr(actor_info, 'username', '')
|
|
100
|
+
name = _attr(actor_info, 'name', '')
|
|
101
|
+
title = _attr(actor_info, 'title', '') or name
|
|
102
|
+
except Exception as exc:
|
|
103
|
+
logger.warning('apify_discover schema fetch error for %s: %s', actor_id, exc)
|
|
104
|
+
return {'error': str(exc)}
|
|
105
|
+
else:
|
|
106
|
+
return {
|
|
107
|
+
'actor_id': f'{username}~{name}',
|
|
108
|
+
'name': name,
|
|
109
|
+
'title': title,
|
|
110
|
+
'username': username,
|
|
111
|
+
'description': _attr(actor_info, 'description', ''),
|
|
112
|
+
'input_schema': input_schema,
|
|
113
|
+
'readme': readme,
|
|
114
|
+
'tip': (f"Use apify_start with actor_id='{username}~{name}' and an input matching the input_schema above."),
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _discover_store(client: Any, query: str) -> dict[str, Any]:
|
|
119
|
+
"""Search the Apify Store by keyword."""
|
|
120
|
+
try:
|
|
121
|
+
result = client.store().list(search=query, limit=10, sort_by='relevance')
|
|
122
|
+
items = _attr(result, 'items') or []
|
|
123
|
+
actors: list[dict[str, Any]] = []
|
|
124
|
+
for item in items:
|
|
125
|
+
stats = _attr(item, 'stats')
|
|
126
|
+
name = _attr(item, 'name', '')
|
|
127
|
+
username = _attr(item, 'username', '')
|
|
128
|
+
title = _attr(item, 'title') or name
|
|
129
|
+
desc = (_attr(item, 'description') or '')[:200]
|
|
130
|
+
# ActorStats.total_runs — see the snake_case note in _discover_actor().
|
|
131
|
+
run_count = _attr(stats, 'total_runs', 0) or 0
|
|
132
|
+
actors.append(
|
|
133
|
+
{
|
|
134
|
+
'actor_id': f'{username}~{name}',
|
|
135
|
+
'name': name,
|
|
136
|
+
'title': title,
|
|
137
|
+
'username': username,
|
|
138
|
+
'description': desc,
|
|
139
|
+
'run_count': run_count,
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
except Exception as exc:
|
|
143
|
+
logger.warning("apify_discover store search error for '%s': %s", query, exc)
|
|
144
|
+
return {'error': str(exc)}
|
|
145
|
+
else:
|
|
146
|
+
return {'actors': actors}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _start_one_run(client: Any, spec: dict[str, Any]) -> dict[str, Any]:
|
|
150
|
+
actor_id = (spec.get('actor_id') or '').strip()
|
|
151
|
+
run_input = spec.get('input') or {}
|
|
152
|
+
label = spec.get('label')
|
|
153
|
+
|
|
154
|
+
if not actor_id:
|
|
155
|
+
return {'_ok': False, 'error': "Missing 'actor_id' in run spec."}
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
run = client.actor(actor_id).start(run_input=run_input)
|
|
159
|
+
entry: dict[str, Any] = {
|
|
160
|
+
'run_id': _attr(run, 'id'),
|
|
161
|
+
'actor_id': actor_id,
|
|
162
|
+
'dataset_id': _attr(run, 'default_dataset_id'),
|
|
163
|
+
'status': _attr(run, 'status'),
|
|
164
|
+
}
|
|
165
|
+
if label:
|
|
166
|
+
entry['label'] = label
|
|
167
|
+
except Exception as exc:
|
|
168
|
+
logger.warning('apify_start error for %s: %s', actor_id, exc)
|
|
169
|
+
err: dict[str, Any] = {'actor_id': actor_id, 'error': str(exc)}
|
|
170
|
+
if label:
|
|
171
|
+
err['label'] = label
|
|
172
|
+
return {'_ok': False, **err}
|
|
173
|
+
else:
|
|
174
|
+
return {'_ok': True, **entry}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _start_handler(args: dict[str, Any]) -> dict[str, Any]:
|
|
178
|
+
from tools.interrupt import is_interrupted
|
|
179
|
+
|
|
180
|
+
if is_interrupted():
|
|
181
|
+
return {'error': 'Interrupted'}
|
|
182
|
+
|
|
183
|
+
run_specs = args.get('runs') or []
|
|
184
|
+
if not run_specs:
|
|
185
|
+
return {'error': "Provide at least one run spec in 'runs'."}
|
|
186
|
+
if len(run_specs) > _MAX_BATCH_RUNS:
|
|
187
|
+
return {'error': f'Batch too large: {len(run_specs)} runs requested, maximum is {_MAX_BATCH_RUNS}.'}
|
|
188
|
+
|
|
189
|
+
client = _get_client()
|
|
190
|
+
started: list[dict[str, Any]] = []
|
|
191
|
+
errors: list[dict[str, Any]] = []
|
|
192
|
+
|
|
193
|
+
for spec in run_specs:
|
|
194
|
+
from tools.interrupt import is_interrupted
|
|
195
|
+
|
|
196
|
+
if is_interrupted():
|
|
197
|
+
break
|
|
198
|
+
outcome = _start_one_run(client, spec)
|
|
199
|
+
ok = outcome.pop('_ok')
|
|
200
|
+
(started if ok else errors).append(outcome)
|
|
201
|
+
|
|
202
|
+
result: dict[str, Any] = {'runs': started}
|
|
203
|
+
if errors:
|
|
204
|
+
result['errors'] = errors
|
|
205
|
+
return result
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
async def _check_run(client: Any, limit: int, ref: dict[str, Any]) -> dict[str, Any]:
|
|
209
|
+
run_id = ref.get('run_id', '')
|
|
210
|
+
actor_id = ref.get('actor_id', '')
|
|
211
|
+
dataset_id = ref.get('dataset_id', '')
|
|
212
|
+
label = ref.get('label')
|
|
213
|
+
|
|
214
|
+
base: dict[str, Any] = {
|
|
215
|
+
'run_id': run_id,
|
|
216
|
+
'actor_id': actor_id,
|
|
217
|
+
'dataset_id': dataset_id,
|
|
218
|
+
}
|
|
219
|
+
if label:
|
|
220
|
+
base['label'] = label
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
run_info = await asyncio.to_thread(client.run(run_id).get)
|
|
224
|
+
|
|
225
|
+
if run_info is None:
|
|
226
|
+
return {**base, '_type': 'error', 'error': 'Run not found.'}
|
|
227
|
+
|
|
228
|
+
status = _attr(run_info, 'status', 'UNKNOWN')
|
|
229
|
+
base['status'] = status
|
|
230
|
+
|
|
231
|
+
if status not in _TERMINAL_STATUSES:
|
|
232
|
+
return {**base, '_type': 'pending'}
|
|
233
|
+
|
|
234
|
+
if status != 'SUCCEEDED':
|
|
235
|
+
return {**base, '_type': 'error', 'error': f'Run ended with status: {status}'}
|
|
236
|
+
|
|
237
|
+
return await _collect_dataset(client, limit, run_id, dataset_id, base)
|
|
238
|
+
|
|
239
|
+
except Exception as exc:
|
|
240
|
+
logger.warning('apify_collect error for run %s: %s', run_id, exc)
|
|
241
|
+
return {**base, '_type': 'error', 'error': str(exc)}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
async def _collect_dataset(
|
|
245
|
+
client: Any, limit: int, run_id: str, dataset_id: str, base: dict[str, Any]
|
|
246
|
+
) -> dict[str, Any]:
|
|
247
|
+
dataset_result = await asyncio.to_thread(client.dataset(dataset_id).list_items, limit=limit)
|
|
248
|
+
items = list(_attr(dataset_result, 'items') or [])
|
|
249
|
+
may_have_more = len(items) == limit
|
|
250
|
+
if may_have_more:
|
|
251
|
+
logger.warning(
|
|
252
|
+
'apify_collect run %s: fetched %d items (hit limit=%d) — '
|
|
253
|
+
'dataset may have more; re-call with a higher limit if needed',
|
|
254
|
+
run_id,
|
|
255
|
+
len(items),
|
|
256
|
+
limit,
|
|
257
|
+
)
|
|
258
|
+
raw = json.dumps(items, indent=2, default=str)
|
|
259
|
+
if len(raw) > _MAX_CONTENT_CHARS:
|
|
260
|
+
raw = raw[:_MAX_CONTENT_CHARS] + '\n\n[…truncated]'
|
|
261
|
+
wrapped = '<<<EXTERNAL_UNTRUSTED_CONTENT>>>\n' + raw + '\n<<<END_EXTERNAL_UNTRUSTED_CONTENT>>>'
|
|
262
|
+
result: dict[str, Any] = {
|
|
263
|
+
**base,
|
|
264
|
+
'_type': 'completed',
|
|
265
|
+
'result_count': len(items),
|
|
266
|
+
'data': wrapped,
|
|
267
|
+
}
|
|
268
|
+
if may_have_more:
|
|
269
|
+
result['may_have_more'] = True
|
|
270
|
+
return result
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
async def _collect_handler(args: dict[str, Any]) -> dict[str, Any]:
|
|
274
|
+
from tools.interrupt import is_interrupted
|
|
275
|
+
|
|
276
|
+
if is_interrupted():
|
|
277
|
+
return {'error': 'Interrupted'}
|
|
278
|
+
|
|
279
|
+
run_refs = args.get('runs') or []
|
|
280
|
+
if not run_refs:
|
|
281
|
+
return {'error': "Provide 'runs' array (from apify_start)."}
|
|
282
|
+
|
|
283
|
+
limit = int(args.get('limit') or _COLLECT_DEFAULT_LIMIT)
|
|
284
|
+
client = _get_client()
|
|
285
|
+
|
|
286
|
+
raw_results = await asyncio.gather(*[_check_run(client, limit, ref) for ref in run_refs])
|
|
287
|
+
|
|
288
|
+
completed: list[dict[str, Any]] = []
|
|
289
|
+
pending: list[dict[str, Any]] = []
|
|
290
|
+
errors: list[dict[str, Any]] = []
|
|
291
|
+
|
|
292
|
+
for r in raw_results:
|
|
293
|
+
t = r.pop('_type', 'error')
|
|
294
|
+
if t == 'pending':
|
|
295
|
+
pending.append(r)
|
|
296
|
+
elif t == 'error':
|
|
297
|
+
errors.append(r)
|
|
298
|
+
else:
|
|
299
|
+
completed.append(r)
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
'all_done': len(pending) == 0,
|
|
303
|
+
'completed': completed,
|
|
304
|
+
'pending': pending,
|
|
305
|
+
'errors': errors,
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# ---------------------------------------------------------------------------
|
|
310
|
+
# Schemas
|
|
311
|
+
# ---------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
_DISCOVER_SCHEMA: dict[str, Any] = {
|
|
314
|
+
'name': 'apify_discover',
|
|
315
|
+
'description': (
|
|
316
|
+
"Search the Apify Store for Actors by keyword, or fetch an Actor's "
|
|
317
|
+
"input schema and README. Provide 'query' to search, or 'actor_id' "
|
|
318
|
+
'to inspect a specific Actor. Actor IDs accept either the unique ID '
|
|
319
|
+
'or the username~actor-name format.'
|
|
320
|
+
),
|
|
321
|
+
'parameters': {
|
|
322
|
+
'type': 'object',
|
|
323
|
+
'properties': {
|
|
324
|
+
'query': {
|
|
325
|
+
'type': 'string',
|
|
326
|
+
'description': "Keywords to search the Apify Store (e.g. 'instagram scraper').",
|
|
327
|
+
},
|
|
328
|
+
'actor_id': {
|
|
329
|
+
'type': 'string',
|
|
330
|
+
'description': (
|
|
331
|
+
'Actor ID to fetch its input schema and README — either the '
|
|
332
|
+
"unique ID or 'username~actor-name' (e.g. 'apify~google-search-scraper')."
|
|
333
|
+
),
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
_START_SCHEMA: dict[str, Any] = {
|
|
340
|
+
'name': 'apify_start',
|
|
341
|
+
'description': (
|
|
342
|
+
'Start one or more Apify Actor runs. Returns run references immediately '
|
|
343
|
+
f'(fire-and-forget). Pass the returned run refs to apify_collect to get results. '
|
|
344
|
+
f'Maximum {_MAX_BATCH_RUNS} runs per call.'
|
|
345
|
+
),
|
|
346
|
+
'parameters': {
|
|
347
|
+
'type': 'object',
|
|
348
|
+
'properties': {
|
|
349
|
+
'runs': {
|
|
350
|
+
'type': 'array',
|
|
351
|
+
'description': f'List of Actor runs to start (max {_MAX_BATCH_RUNS}).',
|
|
352
|
+
'maxItems': _MAX_BATCH_RUNS,
|
|
353
|
+
'items': {
|
|
354
|
+
'type': 'object',
|
|
355
|
+
'properties': {
|
|
356
|
+
'actor_id': {
|
|
357
|
+
'type': 'string',
|
|
358
|
+
'description': 'Actor ID (username~actor-name).',
|
|
359
|
+
},
|
|
360
|
+
'input': {
|
|
361
|
+
'type': 'object',
|
|
362
|
+
'description': 'Actor input parameters.',
|
|
363
|
+
},
|
|
364
|
+
'label': {
|
|
365
|
+
'type': 'string',
|
|
366
|
+
'description': 'Optional label to identify this run in results.',
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
'required': ['actor_id'],
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
'required': ['runs'],
|
|
374
|
+
},
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
_COLLECT_SCHEMA: dict[str, Any] = {
|
|
378
|
+
'name': 'apify_collect',
|
|
379
|
+
'description': (
|
|
380
|
+
'Poll the status of Apify Actor runs started with apify_start. '
|
|
381
|
+
'Returns completed results, still-running refs, and errors. '
|
|
382
|
+
'Re-call with the same run refs until all_done is true.'
|
|
383
|
+
),
|
|
384
|
+
'parameters': {
|
|
385
|
+
'type': 'object',
|
|
386
|
+
'properties': {
|
|
387
|
+
'runs': {
|
|
388
|
+
'type': 'array',
|
|
389
|
+
'description': 'Run references returned by apify_start.',
|
|
390
|
+
'items': {
|
|
391
|
+
'type': 'object',
|
|
392
|
+
'properties': {
|
|
393
|
+
'run_id': {'type': 'string'},
|
|
394
|
+
'actor_id': {'type': 'string'},
|
|
395
|
+
'dataset_id': {'type': 'string'},
|
|
396
|
+
'label': {'type': 'string'},
|
|
397
|
+
},
|
|
398
|
+
'required': ['run_id', 'actor_id', 'dataset_id'],
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
'limit': {
|
|
402
|
+
'type': 'integer',
|
|
403
|
+
'description': (
|
|
404
|
+
f'Max dataset items to fetch per run (default {_COLLECT_DEFAULT_LIMIT}). '
|
|
405
|
+
'Increase if may_have_more is true in a previous response.'
|
|
406
|
+
),
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
'required': ['runs'],
|
|
410
|
+
},
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
# ---------------------------------------------------------------------------
|
|
415
|
+
# String-returning wrappers consumed by register(ctx) in __init__.py
|
|
416
|
+
# ---------------------------------------------------------------------------
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def discover_handler_str(args: dict[str, Any], **_kw: Any) -> str:
|
|
420
|
+
"""String-returning wrapper around `_discover_handler`, for hermes-agent's tool dispatcher."""
|
|
421
|
+
return json.dumps(_discover_handler(args), default=str)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def start_handler_str(args: dict[str, Any], **_kw: Any) -> str:
|
|
425
|
+
"""String-returning wrapper around `_start_handler`, for hermes-agent's tool dispatcher."""
|
|
426
|
+
return json.dumps(_start_handler(args), default=str)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
async def collect_handler_str(args: dict[str, Any], **_kw: Any) -> str:
|
|
430
|
+
"""String-returning wrapper around `_collect_handler`, for hermes-agent's tool dispatcher."""
|
|
431
|
+
return json.dumps(await _collect_handler(args), default=str)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: apify-hermes-agent-plugin
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Hermes Agent plugin for Apify, the world's largest marketplace for AI tools — discover, run, and collect results from Actors, all from natural language
|
|
5
|
+
Author-email: "Apify Technologies s.r.o." <support@apify.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/apify/apify-hermes-agent-plugin
|
|
8
|
+
Project-URL: Source Code, https://github.com/apify/apify-hermes-agent-plugin
|
|
9
|
+
Project-URL: Issue Tracker, https://github.com/apify/apify-hermes-agent-plugin/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/apify/apify-hermes-agent-plugin/blob/main/CHANGELOG.md
|
|
11
|
+
Project-URL: Hermes Agent, https://hermes-agent.nousresearch.com
|
|
12
|
+
Keywords: apify,hermes,hermes-agent,actor,plugin,ai-tools,automation
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: hermes-agent>=0.15.1
|
|
27
|
+
Requires-Dist: apify-client<4,>=3.0.1
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest==9.0.2; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-asyncio==1.3.0; extra == "dev"
|
|
31
|
+
Requires-Dist: ruff<0.17,>=0.15.20; extra == "dev"
|
|
32
|
+
Requires-Dist: ty<0.1,>=0.0.65; extra == "dev"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# apify-hermes-agent-plugin
|
|
36
|
+
|
|
37
|
+
Hermes Agent plugin for [Apify](https://apify.com), the world's largest marketplace for AI tools — discover
|
|
38
|
+
Actors in the Apify Store, start runs, and collect structured results, all from natural language, via
|
|
39
|
+
[Hermes Agent](https://hermes-agent.nousresearch.com).
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
pip install apify-hermes-agent-plugin
|
|
44
|
+
hermes plugins enable apify
|
|
45
|
+
|
|
46
|
+
Then run:
|
|
47
|
+
|
|
48
|
+
hermes apify-setup
|
|
49
|
+
|
|
50
|
+
This prompts for your `APIFY_API_TOKEN` (get one at https://apify.com/account/integrations),
|
|
51
|
+
saves it, and enables the Apify Actors toolset for the CLI. Pass `--token <token>` to skip the
|
|
52
|
+
prompt.
|
|
53
|
+
|
|
54
|
+
## Tools
|
|
55
|
+
|
|
56
|
+
- `apify_discover` — search the Apify Store by keyword, or fetch an Actor's input schema + README by `actor_id`.
|
|
57
|
+
- `apify_start` — fire-and-forget batch Actor starts (up to 10 per call).
|
|
58
|
+
- `apify_collect` — poll run statuses and return completed dataset results.
|
|
59
|
+
|
|
60
|
+
## Development
|
|
61
|
+
|
|
62
|
+
pip install -e ".[dev]"
|
|
63
|
+
pytest
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
CHANGELOG.md
|
|
2
|
+
LICENSE
|
|
3
|
+
MANIFEST.in
|
|
4
|
+
README.md
|
|
5
|
+
pyproject.toml
|
|
6
|
+
src/apify_hermes_agent_plugin/__init__.py
|
|
7
|
+
src/apify_hermes_agent_plugin/cli.py
|
|
8
|
+
src/apify_hermes_agent_plugin/client.py
|
|
9
|
+
src/apify_hermes_agent_plugin/tools.py
|
|
10
|
+
src/apify_hermes_agent_plugin.egg-info/PKG-INFO
|
|
11
|
+
src/apify_hermes_agent_plugin.egg-info/SOURCES.txt
|
|
12
|
+
src/apify_hermes_agent_plugin.egg-info/dependency_links.txt
|
|
13
|
+
src/apify_hermes_agent_plugin.egg-info/entry_points.txt
|
|
14
|
+
src/apify_hermes_agent_plugin.egg-info/requires.txt
|
|
15
|
+
src/apify_hermes_agent_plugin.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
apify_hermes_agent_plugin
|