Skip to content

Commit b8f5d18

Browse files
committed
feat(python): add nemo-relay-plugin worker SDK package
Signed-off-by: Will Killian <wkillian@nvidia.com>
1 parent d884aed commit b8f5d18

22 files changed

Lines changed: 3331 additions & 13 deletions

.github/workflows/ci_python.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,24 @@ jobs:
268268
path: ${{ env.NEMO_RELAY_CI_WORKSPACE_TMP }}/wheels/*.whl
269269
if-no-files-found: error
270270

271+
- name: Package Python plugin SDK wheel
272+
if: ${{ matrix.platform == 'linux-amd64' }}
273+
working-directory: ${{ env.NEMO_RELAY_CI_WORKSPACE }}
274+
run: |
275+
set -e
276+
just \
277+
--set output_dir "${{ env.NEMO_RELAY_CI_WORKSPACE_TMP }}" \
278+
--set ref_name "${NEMO_RELAY_PACKAGE_VERSION}" \
279+
package-python-plugin
280+
281+
- name: Upload Python plugin SDK wheel artifact
282+
if: ${{ matrix.platform == 'linux-amd64' }}
283+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
284+
with:
285+
name: python-plugin-wheel
286+
path: ${{ env.NEMO_RELAY_CI_WORKSPACE_TMP }}/plugin-wheels/*.whl
287+
if-no-files-found: error
288+
271289
- name: Prune uv cache
272290
working-directory: ${{ env.NEMO_RELAY_CI_WORKSPACE }}
273291
run: uv cache prune --ci

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ repos:
5252
hooks:
5353
- id: ty
5454
name: ty (type check)
55-
entry: uv run ty check . --exclude docs/** --exclude fern/** --exclude third_party/** --exclude ./examples/** --exclude .cache/** --exclude .claude/**
55+
entry: uv run ty check . --extra-search-path python/plugin/src --exclude docs/** --exclude fern/** --exclude third_party/** --exclude ./examples/** --exclude .cache/** --exclude .claude/** --exclude python/plugin/src/nemo_relay_plugin/_proto/**
5656
language: system
5757
types: [python]
5858
pass_filenames: false

ATTRIBUTIONS-Python.md

Lines changed: 631 additions & 0 deletions
Large diffs are not rendered by default.

codecov.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,20 @@ component_management:
106106
threshold: 0.5%
107107
base: auto
108108
if_ci_failed: error
109+
- component_id: plugin_sdk
110+
name: Dynamic Plugin SDKs
111+
paths:
112+
- "crates/types/src"
113+
- "crates/plugin/src"
114+
- "crates/worker-proto/src"
115+
- "crates/worker/src"
116+
- "python/plugin/src/nemo_relay_plugin"
117+
statuses:
118+
- type: project
119+
target: 90%
120+
threshold: 0.5%
121+
base: auto
122+
if_ci_failed: error
109123

110124
comment:
111125
after_n_builds: 22
@@ -135,6 +149,7 @@ ignore:
135149
- "target/"
136150
- "**/*.d.ts"
137151
- "**/*.pyi"
152+
- "python/plugin/src/nemo_relay_plugin/_proto/**"
138153
- "python/nemo_relay/lib_native*.dylib.dSYM/**"
139154
# WebAssembly Rust wrappers are covered through wasm-pack execution and
140155
# reported through generated package JavaScript coverage.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
.venv/
5+
__pycache__/
6+
*.py[cod]
7+
*.egg-info/
8+
nemo/
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<!--
2+
SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3+
SPDX-License-Identifier: Apache-2.0
4+
-->
5+
6+
# Python gRPC Worker Plugin
7+
8+
This example shows a Python worker plugin using the `nemo-relay-plugin` SDK. It
9+
registers a tool request intercept, emits a mark event through the host runtime,
10+
and returns a mutated JSON tool request.
11+
12+
## Set Up
13+
14+
From this directory:
15+
16+
```bash
17+
python3 -m venv .venv
18+
. .venv/bin/activate
19+
python -m pip install -e ../../python/plugin -e .
20+
```
21+
22+
The SDK package owns the generated protobuf stubs and gRPC server setup.
23+
24+
## Register With Relay
25+
26+
Point the CLI at this manifest and enable it:
27+
28+
```bash
29+
nemo-relay plugins add ./relay-plugin.toml
30+
nemo-relay plugins enable examples.python_grpc_worker
31+
```
32+
33+
When launching the gateway, point Relay at the Python interpreter that has
34+
`grpcio` installed:
35+
36+
```bash
37+
NEMO_RELAY_PYTHON="$PWD/.venv/bin/python" nemo-relay gateway
38+
```
39+
40+
You can also reference the manifest manually from `plugins.toml`:
41+
42+
```toml
43+
[[plugins.dynamic]]
44+
manifest = "./examples/python-grpc-worker-plugin/relay-plugin.toml"
45+
config = { tag = "demo" }
46+
```
47+
48+
The worker process is started by Relay. Do not run `worker.py` directly unless
49+
you also provide the worker socket, host socket, activation ID, plugin ID, and
50+
activation token environment variables that Relay normally supplies.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
[build-system]
5+
requires = ["setuptools>=68"]
6+
build-backend = "setuptools.build_meta"
7+
8+
[project]
9+
name = "nemo-relay-python-grpc-worker-example"
10+
version = "0.1.0"
11+
description = "Example Python gRPC worker plugin for NeMo Relay"
12+
requires-python = ">=3.11"
13+
dependencies = [
14+
"nemo-relay-plugin>=0.5.0",
15+
]
16+
17+
[tool.setuptools]
18+
py-modules = ["worker"]
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
manifest_version = 1
5+
6+
[plugin]
7+
id = "examples.python_grpc_worker"
8+
kind = "worker"
9+
10+
[compat]
11+
relay = ">=0.5,<1.0"
12+
worker_protocol = "grpc-v1"
13+
14+
[defaults]
15+
enabled = false
16+
17+
[capabilities]
18+
items = ["plugin_worker"]
19+
20+
[load]
21+
runtime = "python"
22+
entrypoint = "worker:main"
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Example Python worker plugin using the nemo-relay-plugin SDK."""
5+
6+
from __future__ import annotations
7+
8+
from nemo_relay_plugin import ConfigDiagnostic, DiagnosticLevel, Json, PluginContext, WorkerPlugin, serve_plugin
9+
10+
11+
class ExamplePythonWorker(WorkerPlugin):
12+
"""Small worker plugin that tags tool request JSON and emits a host mark."""
13+
14+
plugin_id = "examples.python_grpc_worker"
15+
16+
def validate(self, config: Json) -> list[ConfigDiagnostic]:
17+
if isinstance(config, dict) and config.get("reject") is True:
18+
return [
19+
ConfigDiagnostic(
20+
level=DiagnosticLevel.ERROR,
21+
code="examples.python_grpc_worker.rejected",
22+
component=self.plugin_id,
23+
field="reject",
24+
message="Python gRPC worker rejection requested",
25+
)
26+
]
27+
return []
28+
29+
def register(self, ctx: PluginContext, config: Json) -> None:
30+
del config
31+
32+
async def tag_tool_request(tool_name: str, args: Json) -> Json:
33+
await ctx.runtime.emit_mark(
34+
"examples.python_grpc_worker.tool_request",
35+
{"tool_name": tool_name, "source": "python-grpc-worker"},
36+
)
37+
return _tag_json(args)
38+
39+
ctx.register_tool_request_intercept("tag_tool_request", tag_tool_request)
40+
41+
42+
def _tag_json(value: Json) -> Json:
43+
if isinstance(value, dict):
44+
return {**value, "python_grpc_worker": True}
45+
return {"value": value, "python_grpc_worker": True}
46+
47+
48+
async def main() -> None:
49+
"""Entrypoint referenced by relay-plugin.toml."""
50+
await serve_plugin(ExamplePythonWorker())
51+
52+
53+
if __name__ == "__main__":
54+
import asyncio
55+
56+
asyncio.run(main())

0 commit comments

Comments
 (0)