Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ These are **not bundled with `hermes-agent`**. The core repo ships only the plug

| Plugin | Surface | Demonstrates |
|---|---|---|
| [`plugin-tool-example`](./plugin-tool-example) | `ctx.register_tool()` | A model-callable tool — JSON Schema, argument validation, structured `tool_result()` / `tool_error()` responses |
| [`plugin-llm-example`](./plugin-llm-example) | `ctx.llm.complete_structured()` | Host-owned structured LLM calls — typed text/image input, JSON Schema validation, trust-gate config |
| [`plugin-llm-async-example`](./plugin-llm-async-example) | `ctx.llm.acomplete()` + `asyncio.gather()` | Async LLM lane — concurrent forward + sentiment + back-translation pass for `/translate` |
| [`example-dashboard`](./example-dashboard) | `dashboard/manifest.json` | Bare-minimum dashboard plugin — a tab, a slot injection, a backend route |
Expand All @@ -21,12 +22,14 @@ Each directory is a self-contained plugin. To run one in your own Hermes Agent s
git clone https://github.com/NousResearch/hermes-example-plugins.git

# pick whichever you want
cp -r hermes-example-plugins/plugin-tool-example ~/.hermes/plugins/
cp -r hermes-example-plugins/plugin-llm-example ~/.hermes/plugins/
cp -r hermes-example-plugins/plugin-llm-async-example ~/.hermes/plugins/
cp -r hermes-example-plugins/example-dashboard ~/.hermes/plugins/
cp -r hermes-example-plugins/strike-freedom-cockpit ~/.hermes/plugins/

# enable any with a slash command surface
# enable whichever examples you copied
hermes plugins enable plugin-tool-example
hermes plugins enable plugin-llm-example
hermes plugins enable plugin-llm-async-example
```
Expand All @@ -41,6 +44,7 @@ Pair each plugin in this repo with its docs page:

| Plugin here | Docs page |
|---|---|
| `plugin-tool-example` | [Build a Hermes Plugin](https://hermes-agent.nousresearch.com/docs/guides/build-a-hermes-plugin) |
| `plugin-llm-example` | [Plugin LLM Access](https://hermes-agent.nousresearch.com/docs/developer-guide/plugin-llm-access) |
| `plugin-llm-async-example` | [Plugin LLM Access](https://hermes-agent.nousresearch.com/docs/developer-guide/plugin-llm-access) |
| `example-dashboard` | [Extending the Dashboard](https://hermes-agent.nousresearch.com/docs/user-guide/features/extending-the-dashboard) |
Expand Down
67 changes: 67 additions & 0 deletions plugin-tool-example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# plugin-tool-example

Reference plugin showing the smallest production-shaped `ctx.register_tool()`
surface: one deterministic tool, one JSON Schema, one handler.

## What it does

Registers a model-callable `text_stats` tool that summarizes a text snippet:

```json
{
"success": true,
"characters": 43,
"lines": 2,
"words": 8,
"unique_words": 7,
"top_words": [
{"word": "hermes", "count": 2},
{"word": "agent", "count": 1}
]
}
```

This is intentionally plain Python. The example is about the plugin boundary,
not the analysis itself:

- the tool schema tells the model when and how to call it,
- the handler validates user-supplied arguments,
- `tool_result()` and `tool_error()` keep responses machine-readable,
- `register(ctx)` exposes the tool under its own `tool_example` toolset.

## How it works

```python
ctx.register_tool(
name="text_stats",
toolset="tool_example",
schema=TEXT_STATS_SCHEMA,
handler=_handle_text_stats,
description="Count lines, words, and common tokens in a text snippet.",
)
```

The handler accepts the same `args: dict, **kwargs` shape used by bundled
Hermes tools. It rejects empty or oversized payloads, clamps `top_n`, and
returns stable JSON so the model can use the result without reparsing prose.

## Try it

```bash
git clone https://github.com/NousResearch/hermes-example-plugins.git
cp -r hermes-example-plugins/plugin-tool-example ~/.hermes/plugins/
hermes plugins enable plugin-tool-example
```

Then start a Hermes session and ask for text statistics. The model can call
`text_stats` when the `tool_example` toolset is available.

## Files

| File | Purpose |
|---|---|
| `__init__.py` | Tool schema, handler, and `register(ctx)` entry point |
| `plugin.yaml` | Plugin manifest |

For a full multi-file tutorial that also covers hooks, CLI commands, and data
files, see [Build a Hermes Plugin](https://hermes-agent.nousresearch.com/docs/guides/build-a-hermes-plugin).
103 changes: 103 additions & 0 deletions plugin-tool-example/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""
plugin-tool-example - minimal reference plugin for ``ctx.register_tool()``.

Demonstrates the smallest production-shaped general-tool plugin:

* one JSON Schema that tells the model what the tool accepts,
* one deterministic handler with argument validation,
* structured JSON responses via ``tool_result`` / ``tool_error``,
* one ``ctx.register_tool`` call in ``register(ctx)``.
"""

from __future__ import annotations

import logging
import re
from collections import Counter
from typing import Any

from tools.registry import tool_error, tool_result

logger = logging.getLogger(__name__)

_MAX_TEXT_CHARS = 20_000
_WORD_RE = re.compile(r"[A-Za-z0-9]+(?:['-][A-Za-z0-9]+)?")


TEXT_STATS_SCHEMA = {
"description": (
"Count lines, words, unique words, and common tokens in a text snippet. "
"Use this when the user asks for lightweight text statistics rather than "
"semantic analysis."
),
"parameters": {
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "Text to analyze, up to 20,000 characters.",
},
"top_n": {
"type": "integer",
"description": "How many common words to return (1-10). Defaults to 5.",
"minimum": 1,
"maximum": 10,
},
},
"required": ["text"],
"additionalProperties": False,
},
}


def _coerce_top_n(raw: Any) -> int:
try:
value = int(raw)
except (TypeError, ValueError):
value = 5
return max(1, min(10, value))


def _handle_text_stats(args: dict, **kwargs: Any) -> str:
"""Return deterministic text metrics for a caller-provided snippet."""
del kwargs

text = args.get("text")
if not isinstance(text, str) or not text.strip():
return tool_error("text must be a non-empty string")
if len(text) > _MAX_TEXT_CHARS:
return tool_error(
"text exceeds the 20,000 character limit",
max_chars=_MAX_TEXT_CHARS,
)

top_n = _coerce_top_n(args.get("top_n", 5))
words = [match.group(0).casefold() for match in _WORD_RE.finditer(text)]
counts = Counter(words)
top_words = [
{"word": word, "count": count}
for word, count in sorted(counts.items(), key=lambda item: (-item[1], item[0]))[
:top_n
]
]

return tool_result({
"success": True,
"characters": len(text),
"lines": len(text.splitlines()) or 1,
"words": len(words),
"unique_words": len(counts),
"top_words": top_words,
})


def register(ctx: Any) -> None:
"""Plugin entry point - expose one model-callable tool."""
ctx.register_tool(
name="text_stats",
toolset="tool_example",
schema=TEXT_STATS_SCHEMA,
handler=_handle_text_stats,
description="Count lines, words, and common tokens in a text snippet.",
)
logger.debug("plugin-tool-example: registered text_stats")
7 changes: 7 additions & 0 deletions plugin-tool-example/plugin.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
name: plugin-tool-example
version: 1.0.0
description: "Reference plugin for ctx.register_tool() - registers one deterministic text_stats tool with JSON Schema validation and structured responses."
author: NousResearch
hooks: []
provides_tools:
- text_stats