How to use custom components with the Plugin System#
Prerequisites
This guide assumes you are familiar with the following concepts:
Additionally, you need to have Python 3.10+ installed.
Overview#
Agent Spec supports a list of core native components to build your LLM-powered assistants, for instance a list of pre-defined Nodes (Agent Spec Spec, API Reference).
You might however want to go beyond the list of components supported in the base specification and use additional components while being able to use the Agent Spec Assistant Configuration system. The pyagentspec plugin system is designed to address this use case. Example uses of the plugin system include:
New Large Language Model (LLM) configurations (by extending the
LlmConfig
component);New nodes for
Flows
(by extending theNode
component);New tools (by extending the
Tool
component);Extensions to the
Agent
andFlow
components.
Overview of the Agent Spec plugin system.#
This guide demonstrates how to support a custom PluginRegexNode
in Flows
to extract information
from a raw text using a regular expression (regex). You will:
Create a custom
PluginRegexNode
Agent Spec node and (de)serialization plugins so your assistant can be correctly saved and loaded;Use the custom component to create an assistant that can extract information using regex;
Export your assistant configuration to Agent Spec JSON/YAML.
Load and execute your agent in a runtime executor (WayFlow).
Important
The current plugin system enables the serialization and deserialization of custom components for Agent Spec.
To fully support custom components, you also need to:
Support the feature in your Agent Spec runtime implementation.
Support the conversion between the custom Agent Spec plugin component and its corresponding implementation in your runtime implementation.
Basic implementation#
Define custom Agent Spec components#
First you need to create the corresponding Agent Spec components.
from typing import ClassVar, List
from pyagentspec.flows.node import Node
from pyagentspec.property import Property, StringProperty
class PluginRegexNode(Node):
"""Node to extract information from a raw text using a regular expression (regex)."""
regex_pattern: str
"""Specify the regex pattern for searching in the input text."""
DEFAULT_INPUT_KEY: ClassVar[str] = "text"
"""Input key for the name to transition to next."""
DEFAULT_OUTPUT_KEY: ClassVar[str] = "output"
"""Input key for the name to transition to next."""
def _get_inferred_inputs(self) -> List[Property]:
input_title = self.inputs[0].title if self.inputs else self.DEFAULT_INPUT_KEY
return [StringProperty(title=input_title)]
def _get_inferred_outputs(self) -> List[Property]:
output_title = self.outputs[0].title if self.outputs else self.DEFAULT_OUTPUT_KEY
return [StringProperty(title=output_title)]
API Reference: Node | Property
Here, the PluginRegexNode
is the custom node that we intent to use in a Flow.
Create an assistant using the custom component#
The decision mechanism of Agents is powered by a Large Language Model. Defining the agent with Agent Spec requires to pass the configuration for the LLM. There are several options such as using OCI GenAI Service or self hosting a model with vLLM.
Start by defining the LLM configuration to be shared across all agents. This example uses a vLLM instance running Llama3.3 70B.
from pyagentspec.llms import OciGenAiConfig
from pyagentspec.llms.ociclientconfig import OciClientConfigWithApiKey
client_config = OciClientConfigWithApiKey(
name="Oci Client Config",
service_endpoint="https://url-to-service-endpoint.com",
auth_profile="DEFAULT",
auth_file_location="~/.oci/config"
)
llm_config = OciGenAiConfig(
name="Oci GenAI Config",
model_id="provider.model-id",
compartment_id="compartment-id",
client_config=client_config,
)
from pyagentspec.llms import VllmConfig
llm_config = VllmConfig(
name="Llama 3.1 8B instruct",
url="VLLM_URL",
model_id="model-id",
)
from pyagentspec.llms import OllamaConfig
llm_config = OllamaConfig(
name="Ollama Config",
model_id="model-id",
)
Here, you will create a Flow that generates an answer to a query using reasoning thoughts, and then parses the output to return the final answer.
Use the PluginRegexNode
defined previously to create your assistant.
from pyagentspec.flows.edges import ControlFlowEdge, DataFlowEdge
from pyagentspec.flows.flow import Flow
from pyagentspec.flows.nodes import EndNode, LlmNode, StartNode
from pyagentspec.property import StringProperty
llmoutput_property = StringProperty(title="llm_output", default="ERROR")
parsedoutput_property = StringProperty(title="output", default="<result>ERROR</result>")
start_node = StartNode(id="start", name="start", inputs=[])
llm_node = LlmNode(
id="llm",
name="llm",
llm_config=llm_config,
prompt_template=(
"What is the result of 100+(454-3). Think step by step and then give "
"your answer between <result>...</result> delimiters"
),
outputs=[llmoutput_property],
)
regex_node = PluginRegexNode(
id="regex",
name="regex",
regex_pattern=r"<result>(.*)</result>",
outputs=[parsedoutput_property],
)
end_node = EndNode(id="end", name="end", outputs=[parsedoutput_property])
assistant = Flow(
id="regex_flow",
name="regex_flow",
start_node=start_node,
nodes=[start_node, llm_node, regex_node, end_node],
control_flow_connections=[
ControlFlowEdge(id="start_llm", name="start->llm", from_node=start_node, to_node=llm_node),
ControlFlowEdge(id="llm_regex", name="llm->regex", from_node=llm_node, to_node=regex_node),
ControlFlowEdge(id="regex_end", name="regex->end", from_node=regex_node, to_node=end_node),
],
data_flow_connections=[
DataFlowEdge(
name="edge",
source_node=llm_node,
source_output="llm_output",
destination_node=regex_node,
destination_input="text"
),
DataFlowEdge(
name="edge",
source_node=regex_node,
source_output="output",
destination_node=end_node,
destination_input="output"
),
]
)
Register plugins and export the agent configuration#
You need to register (de)serialization plugins so your custom node can be correctly saved and loaded using the Agent Spec serialization format.
from pyagentspec.serialization.pydanticdeserializationplugin import (
PydanticComponentDeserializationPlugin,
)
from pyagentspec.serialization.pydanticserializationplugin import PydanticComponentSerializationPlugin
example_serialization_plugin = PydanticComponentSerializationPlugin(
component_types_and_models={
PluginRegexNode.__name__: PluginRegexNode,
}
)
example_deserialization_plugin = PydanticComponentDeserializationPlugin(
component_types_and_models={
PluginRegexNode.__name__: PluginRegexNode,
}
)
API Reference: PydanticComponentSerializationPlugin | PydanticComponentDeserializationPlugin.
This enables PyAgentSpec’s serializer and deserializer to recognize your custom classes. In this example, since the components are directly inheriting from the Pydantic model Component, you should use the already existing (de)serialization plugins.
For more advanced use, you can implement custom plugins by inheriting from ComponentSerializationPlugin and ComponentDeserializationPlugin
You can then serialize your assistant to its Agent Spec JSON/YAML configuration using the registered plugins:
from pyagentspec.serialization import AgentSpecSerializer
serialized_assistant = AgentSpecSerializer(plugins=[example_serialization_plugin]).to_json(assistant)
with open("assistant_config.json", "w") as f:
f.write(serialized_assistant)
API Reference: AgentSpecSerializer
Here is what the Agent Spec representation will look like ↓
Click here to see the assistant configuration.
{
"component_type": "Flow",
"id": "regex_flow",
"name": "regex_flow",
"description": null,
"metadata": {},
"inputs": [],
"outputs": [
{
"title": "output",
"default": "<result>ERROR</result>",
"type": "string"
}
],
"start_node": {
"$component_ref": "start"
},
"nodes": [
{
"$component_ref": "start"
},
{
"$component_ref": "llm"
},
{
"$component_ref": "regex"
},
{
"$component_ref": "end"
}
],
"state": [],
"control_flow_connections": [
{
"component_type": "ControlFlowEdge",
"id": "start_llm",
"name": "start->llm",
"description": null,
"metadata": {},
"from_node": {
"$component_ref": "start"
},
"from_branch": null,
"to_node": {
"$component_ref": "llm"
}
},
{
"component_type": "ControlFlowEdge",
"id": "llm_regex",
"name": "llm->regex",
"description": null,
"metadata": {},
"from_node": {
"$component_ref": "llm"
},
"from_branch": null,
"to_node": {
"$component_ref": "regex"
}
},
{
"component_type": "ControlFlowEdge",
"id": "regex_end",
"name": "regex->end",
"description": null,
"metadata": {},
"from_node": {
"$component_ref": "regex"
},
"from_branch": null,
"to_node": {
"$component_ref": "end"
}
}
],
"data_flow_connections": [
{
"component_type": "DataFlowEdge",
"id": "a5811a1e-710d-4c88-927c-6249b3186e95",
"name": "edge",
"description": null,
"metadata": {},
"source_node": {
"$component_ref": "llm"
},
"source_output": "llm_output",
"destination_node": {
"$component_ref": "regex"
},
"destination_input": "text"
},
{
"component_type": "DataFlowEdge",
"id": "14ed74b6-e344-4353-ad10-6afaab9d5dc4",
"name": "edge",
"description": null,
"metadata": {},
"source_node": {
"$component_ref": "regex"
},
"source_output": "output",
"destination_node": {
"$component_ref": "end"
},
"destination_input": "output"
}
],
"$referenced_components": {
"llm": {
"component_type": "LlmNode",
"id": "llm",
"name": "llm",
"description": null,
"metadata": {},
"inputs": [],
"outputs": [
{
"title": "llm_output",
"default": "ERROR",
"type": "string"
}
],
"branches": [
"next"
],
"llm_config": {
"component_type": "VllmConfig",
"id": "34f14c5d-5af0-4ca5-b3f1-3e61b4280fda",
"name": "Vllm model",
"description": null,
"metadata": {},
"default_generation_parameters": null,
"url": "vllm_url",
"model_id": "model_id"
},
"prompt_template": "What is the result of 100+(454-3). Think step by step and then give your answer between <result>...</result> delimiters"
},
"start": {
"component_type": "StartNode",
"id": "start",
"name": "start",
"description": null,
"metadata": {},
"inputs": [],
"outputs": [],
"branches": [
"next"
]
},
"regex": {
"component_type": "PluginRegexNode",
"id": "regex",
"name": "regex",
"description": null,
"metadata": {},
"inputs": [
{
"title": "text",
"type": "string"
}
],
"outputs": [
{
"title": "output",
"default": "<result>ERROR</result>",
"type": "string"
}
],
"branches": [
"next"
],
"regex_pattern": "<result>(.*)</result>",
"component_plugin_name": "PydanticComponentPlugin",
"component_plugin_version": "25.3.0.dev2"
},
"end": {
"component_type": "EndNode",
"id": "end",
"name": "end",
"description": null,
"metadata": {},
"inputs": [
{
"title": "output",
"default": "<result>ERROR</result>",
"type": "string"
}
],
"outputs": [
{
"title": "output",
"default": "<result>ERROR</result>",
"type": "string"
}
],
"branches": [],
"branch_name": "next"
}
},
"agentspec_version": "25.4.1"
}
component_type: Flow
id: regex_flow
name: regex_flow
description: null
metadata: {}
inputs: []
outputs:
- title: output
default: <result>ERROR</result>
type: string
start_node:
$component_ref: start
nodes:
- $component_ref: start
- $component_ref: llm
- $component_ref: regex
- $component_ref: end
control_flow_connections:
- component_type: ControlFlowEdge
id: start_llm
name: start->llm
description: null
metadata: {}
from_node:
$component_ref: start
from_branch: null
to_node:
$component_ref: llm
- component_type: ControlFlowEdge
id: llm_regex
name: llm->regex
description: null
metadata: {}
from_node:
$component_ref: llm
from_branch: null
to_node:
$component_ref: regex
- component_type: ControlFlowEdge
id: regex_end
name: regex->end
description: null
metadata: {}
from_node:
$component_ref: regex
from_branch: null
to_node:
$component_ref: end
data_flow_connections:
- component_type: DataFlowEdge
id: c729d594-865c-461d-987e-e4729d7c2af5
name: edge
description: null
metadata: {}
source_node:
$component_ref: llm
source_output: llm_output
destination_node:
$component_ref: regex
destination_input: text
- component_type: DataFlowEdge
id: 84d09677-d339-46d5-8a9a-9688de6e07ef
name: edge
description: null
metadata: {}
source_node:
$component_ref: regex
source_output: output
destination_node:
$component_ref: end
destination_input: output
$referenced_components:
start:
component_type: StartNode
id: start
name: start
description: null
metadata: {}
inputs: []
outputs: []
branches:
- next
llm:
component_type: LlmNode
id: llm
name: llm
description: null
metadata: {}
inputs: []
outputs:
- title: llm_output
default: ERROR
type: string
branches:
- next
llm_config:
component_type: VllmConfig
id: 2d156995-4289-4ef9-a3ad-7ed4618b5a79
name: Vllm model
description: null
metadata: {}
default_generation_parameters: null
url: url
model_id: model_id
prompt_template: What is the result of 100+(454-3). Think step by step and then
give your answer between <result>...</result> delimiters
regex:
component_type: PluginRegexNode
id: regex
name: regex
description: null
metadata: {}
inputs:
- title: text
type: string
outputs:
- title: output
default: <result>ERROR</result>
type: string
branches:
- next
regex_pattern: <result>(.*)</result>
component_plugin_name: PydanticComponentPlugin
component_plugin_version: 25.3.0.dev2
end:
component_type: EndNode
id: end
name: end
description: null
metadata: {}
inputs:
- title: output
default: <result>ERROR</result>
type: string
outputs:
- title: output
default: <result>ERROR</result>
type: string
branches: []
branch_name: next
agentspec_version: 25.4.1
Load and execute your assistant from a configuration file#
See also
For more details, see the guide on How to Execute an Agent Spec Configuration with WayFlow.
To actually run the assistant, load the JSON or YAML configuration using the deserialization plugins defined above.
from wayflowcore.flow import Flow as RuntimeFlow
from wayflowcore.agentspec import AgentSpecLoader
with open("assistant_config.json") as f:
agentspec_export = f.read()
# agentspec_loader = AgentSpecLoader(plugins=[example_deserialization_plugin])
assistant: RuntimeFlow = agentspec_loader.load_yaml(agentspec_export)
Now you can run your assistant! The assistant calls an LLM with the pre-defined request, and returns the parsed output.
from wayflowcore.executors.executionstatus import FinishedStatus
inputs = {}
conversation = assistant.start_conversation(inputs)
status = conversation.execute()
if isinstance(status, FinishedStatus):
outputs = status.output_values
print(f"Assistant outputs: {outputs['output']}")
else:
print(f"ERROR: Expected 'FinishedStatus', got {status.__class__.__name__}")
# Assistant outputs: 551
Recap#
This guide covered how to:
Create a custom Agent Spec component and its corresponding (de)serialization plugin.
Build and export an assistant (Flow) with a custom regex node using the custom component and plugin.
Load and execute the assistant in a runtime executor.
Below is the complete code from this guide.
from typing import ClassVar, List
from pyagentspec.flows.node import Node
from pyagentspec.property import Property, StringProperty
class PluginRegexNode(Node):
"""Node to extract information from a raw text using a regular expression (regex)."""
regex_pattern: str
"""Specify the regex pattern for searching in the input text."""
DEFAULT_INPUT_KEY: ClassVar[str] = "text"
"""Input key for the name to transition to next."""
DEFAULT_OUTPUT_KEY: ClassVar[str] = "output"
"""Input key for the name to transition to next."""
def _get_inferred_inputs(self) -> List[Property]:
input_title = self.inputs[0].title if self.inputs else self.DEFAULT_INPUT_KEY
return [StringProperty(title=input_title)]
def _get_inferred_outputs(self) -> List[Property]:
output_title = self.outputs[0].title if self.outputs else self.DEFAULT_OUTPUT_KEY
return [StringProperty(title=output_title)]
from pyagentspec.flows.edges import ControlFlowEdge, DataFlowEdge
from pyagentspec.flows.flow import Flow
from pyagentspec.flows.nodes import EndNode, LlmNode, StartNode
from pyagentspec.property import StringProperty
llmoutput_property = StringProperty(title="llm_output", default="ERROR")
parsedoutput_property = StringProperty(title="output", default="<result>ERROR</result>")
start_node = StartNode(id="start", name="start", inputs=[])
llm_node = LlmNode(
id="llm",
name="llm",
llm_config=llm_config,
prompt_template=(
"What is the result of 100+(454-3). Think step by step and then give "
"your answer between <result>...</result> delimiters"
),
outputs=[llmoutput_property],
)
regex_node = PluginRegexNode(
id="regex",
name="regex",
regex_pattern=r"<result>(.*)</result>",
outputs=[parsedoutput_property],
)
end_node = EndNode(id="end", name="end", outputs=[parsedoutput_property])
assistant = Flow(
id="regex_flow",
name="regex_flow",
start_node=start_node,
nodes=[start_node, llm_node, regex_node, end_node],
control_flow_connections=[
ControlFlowEdge(id="start_llm", name="start->llm", from_node=start_node, to_node=llm_node),
ControlFlowEdge(id="llm_regex", name="llm->regex", from_node=llm_node, to_node=regex_node),
ControlFlowEdge(id="regex_end", name="regex->end", from_node=regex_node, to_node=end_node),
],
data_flow_connections=[
DataFlowEdge(
name="edge",
source_node=llm_node,
source_output="llm_output",
destination_node=regex_node,
destination_input="text"
),
DataFlowEdge(
name="edge",
source_node=regex_node,
source_output="output",
destination_node=end_node,
destination_input="output"
),
]
)
from pyagentspec.serialization.pydanticdeserializationplugin import (
PydanticComponentDeserializationPlugin,
)
from pyagentspec.serialization.pydanticserializationplugin import PydanticComponentSerializationPlugin
example_serialization_plugin = PydanticComponentSerializationPlugin(
component_types_and_models={
PluginRegexNode.__name__: PluginRegexNode,
}
)
example_deserialization_plugin = PydanticComponentDeserializationPlugin(
component_types_and_models={
PluginRegexNode.__name__: PluginRegexNode,
}
)
from pyagentspec.serialization import AgentSpecSerializer
serialized_assistant = AgentSpecSerializer(plugins=[example_serialization_plugin]).to_json(assistant)
with open("assistant_config.json", "w") as f:
f.write(serialized_assistant)
from wayflowcore.flow import Flow as RuntimeFlow
from wayflowcore.agentspec import AgentSpecLoader
with open("assistant_config.json") as f:
agentspec_export = f.read()
# agentspec_loader = AgentSpecLoader(plugins=[example_deserialization_plugin])
assistant: RuntimeFlow = agentspec_loader.load_yaml(agentspec_export)
from wayflowcore.executors.executionstatus import FinishedStatus
inputs = {}
conversation = assistant.start_conversation(inputs)
status = conversation.execute()
if isinstance(status, FinishedStatus):
outputs = status.output_values
print(f"Assistant outputs: {outputs['output']}")
else:
print(f"ERROR: Expected 'FinishedStatus', got {status.__class__.__name__}")
# Assistant outputs: 551
Next steps#
Having learned how to build assistants with custom Agent Spec components using the plugin system, you may now proceed to How to Build an Orchestrator-Workers Agents Pattern.