title | description | icon |
---|---|---|
Nodes Python SDK |
Build independent agent nodes with advanced functionality |
code |
pip install openagents-node-sdk
from openagents import JobRunner,OpenAgentsNode,NodeConfig,RunnerConfig
- Definition of a runner class:
# Define a runner
class MyRunner (JobRunner):
def __init__(self):
# Set the runner metadata, template and filter
super().__init__(
RunnerConfig(
meta={ # ...
Metadata are required information about the node. You can set them in the "meta" object.
The “kind” value is the job kind, you can use one from the NIP-90 standard jobs
or "5003" that is the custom OpenAgents generic job kind.
All the other fields are free to be filled in as you like.
meta={
"kind": 5003,
"name": "My Action",
"description": "This is a new action",
"tos": "https://example.com/tos",
"privacy": "https://example.com/privacy",
"picture": "https://example.com/icon.png",
"tags": ["tag1","tag2"],
"website": "https://example.com",
"author": "John Doe",
},
We currently use the following tags: - tool: if the action can be considered as a standalone tool for an LLM
But you can use any tag you like.
Filters are regular expressions used to filter the jobs that the node can execute. You can check the protocol definition for a list of filters that you can use.
filter={
"filterByKind": "5003",
#AND
"filterByRunOn": "my-new-action"
},
If you are writing a runner for a 5003 job kind, you should always use "filterByRunOn" with a custom name of you choice that you will use in the "run-on" parameter in the template (see below).
Runner templates are mustache templates based on NIP-01 and NIP-90 that define the structure of the event that must be sent to nostr to invoke the runner.
Inputs are defined as one ore more ["i"]
tags:
["i", "{{value}}", "{{type}}", "", "query"],
They need a "value", a "type" and a "name or maker".
As you can see value and type here are mustache's tags, we will see how they are replaced in the next section.
"query" is the marker, it can be anything you want and its only purpose is to help you identify the input in the runner logic.
There can also be 0 or more parameters, prefixed with param, then the key (name) and the value:
["param","run-on", "my-new-action" ],
A param of type run-on
is mandatory if you create a 5003 event and should be the same as the filterByRunOn
in the filter.
Events can contain anything that is defined in the NIPs as they represent standard nostr events.
The full template will look something like this:
template="""
{
"kind": {{meta.kind}},
"created_at": {{sys.timestamp_seconds}},
"tags": [
["param","run-on", "my-new-action" ],
["param", "k", "{{in.k}}"],
["output" , "{{in.outputType}}"],
{{#in.queries}}
["i", "{{value}}", "{{type}}", "", "query"],
{{/in.queries}}
["expiration", "{{sys.expiration_timestamp_seconds}}"],
],
"content":""
}"""
As you saw in the template, we use mustache tags prefixed with in
, out
and sys
, these match the keys in the sockets
object.
You need to define the in
and out
socket layouts in the runner class:
sockets={
"in": {
"k": {
"title": "MyNumber",
"description":"This is a number",
"type":"integer",
"default": 0
},
"queries": {
"title": "MyArray",
"type":"array",
"description":"This is an array of strings",
"items": {
"type":"string"
}
},
"outputType": {
"title": "Output Type",
"type": "string",
"description": "The output type of the event",
"default": "application/json"
}
},
"out": {
"output": {
"title": "Output",
"type": "string",
"description": "The output content"
}
}
}
This object defines the structure of the inputs and outputs of the runner and will be used by the software, that wish to interact with it, together with the template to create a nostr event.
The **socket** object is in JSON schema, you can check [the official documentation for more details](https://json-schema.org/).These values can be accessed in the template using the mustache syntax by prefixing the key with in
or out
as we've seen in the previous section.
The sys
socket is defined by the environment, so you don't have to define it in the sockets
object, but you can use its values in the template:
sys.timestamp_seconds
is the current timestamp in secondssys.expiration_timestamp_seconds
is the expiration timestamp for the event in seconds
API documentation for the SDK methods.
The init
method intitializes the runner class:
async def init(self, node):
# Initialize class
pass
These methods receive as argument a ctx
JobContext object.
The ctx
object contains several methods that can be used to get useful information related to the job (including inputs and parameters) and to comunicate with the pool's api, see the documentation for more details.
canRun
is a filter. Can be used to perform checks before deciding whether to run the job (bool
):
async def canRun(self,ctx):
# Custom job filtering logic
return True
preRun
is a hook that runs before run
. It can be useful depending on the case:
async def preRun(self, ctx):
# Do something before running the job
pass
postRun
executes some code after the main logic in run
has been executed. It can be useful depending on the case:
async def postRun(self, ctx):
# Do something after running the job
pass
run
is the main method, the one on which the node's core logic must be implemented:
async def run(self,ctx):
# Do something
print("Running job",job.id)
# Finish the job
jobOutput=""
return jobOutput
A single instance of the JobRunner is used to process each compatible job, by default this happens synchronously (ie. the next job will start after the end of the previous job).
It is possible to configure the runner to run the jobs in parallel by calling self.setRunInParallel(True)
inside the init
method.
Note: Parallel runners should be written in valid non-blocking asyncio code.
Now that you have defined the runner class, you can write the code to initialize and start the runner, you can do it in the same file:
# Initialize the node
myNode = OpenAgentsNode(NodeConfig(
meta={
"name": "My Node",
"description": "This is a new node",
"version": "1.0"
}
))
name, description and version are required fields for the node metadata, you can set them as you like.
Next you need to register the runner
# Register the runner (you can register multiple runners)
myNode.registerRunner(MyRunner())
And finally start the node:
# Start the node
myNode.start()
You can see a full example of a runner here.
If you have followed up to this point, you should have a runner class and a node ready to be executed.
By default the node will connect to OpenAgents open pool (playground.openagents.com 6021 ssl), this can be changed by setting the following environment variables:*
POOL_ADDRESS="playground.openagents.com" # custom pool address
POOL_PORT="6021" # custom pool port
POOL_SSL="true" # or "false"
You can run the node with the following command:
python myNode.py