Skip to content
Closed
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
44 changes: 44 additions & 0 deletions nodes/src/nodes/prompt_template/IGlobal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# =============================================================================
# MIT License
# Copyright (c) 2026 Aparavi Software AG
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# =============================================================================

from rocketlib import IGlobalBase, OPEN_MODE
from ai.common.config import Config


class IGlobal(IGlobalBase):
config = None

def beginGlobal(self):
if self.IEndpoint.endpoint.openMode == OPEN_MODE.CONFIG:
pass
else:
import os
from depends import depends # type: ignore

requirements = os.path.dirname(os.path.realpath(__file__)) + '/requirements.txt'
depends(requirements)

self.config = Config.getNodeConfig(self.glb.logicalType, self.glb.connConfig)

def endGlobal(self):
self.config = None
85 changes: 85 additions & 0 deletions nodes/src/nodes/prompt_template/IInstance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# =============================================================================
# MIT License
# Copyright (c) 2026 Aparavi Software AG
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# =============================================================================

from rocketlib import IInstanceBase
from .IGlobal import IGlobal
from .template_engine import render
from ai.common.schema import Question
from rocketlib import debug, Entry


class IInstance(IInstanceBase):
IGlobal: IGlobal

def __init__(self):
"""Initialize the prompt template node instance state."""
super().__init__()
self.collected_text: list[str] = []
self.question = Question()

def open(self, entry: Entry):
pass

def _get_template_context(self) -> dict:
"""Build the context dictionary for template rendering."""
config = self.IGlobal.config
if config is None:
config = {}
raw_variables = config.get('variables', {})
variables = dict(raw_variables) if isinstance(raw_variables, dict) else {}

# Add collected text as 'input' if available
if self.collected_text:
variables.setdefault('input', '\n'.join(self.collected_text))

return variables

def writeQuestions(self, question: Question):
for q in question.questions:
self.question.addQuestion(q.text)

def writeText(self, text: str):
self.collected_text.append(text)

def closing(self):
try:
config = self.IGlobal.config or {}
template = config.get('template', '{{input}}')
context = self._get_template_context()

# Render questions with full context now that all text has arrived
if self.question.questions:
rendered_question = Question()
for q in self.question.questions:
context['question'] = q.text
rendered = render(template, context)
rendered_question.addQuestion(rendered)
self.instance.writeQuestions(rendered_question)
# If we only have text input, render and output as text
elif self.collected_text:
rendered = render(template, context)
self.instance.writeText(rendered)

except Exception as e:
debug(f'Error in prompt_template node: {e}')
raise
27 changes: 27 additions & 0 deletions nodes/src/nodes/prompt_template/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# =============================================================================
# MIT License
# Copyright (c) 2026 Aparavi Software AG
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# =============================================================================

from .IGlobal import IGlobal
from .IInstance import IInstance

__all__ = ['IGlobal', 'IInstance']
1 change: 1 addition & 0 deletions nodes/src/nodes/prompt_template/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# No external dependencies — stdlib only
195 changes: 195 additions & 0 deletions nodes/src/nodes/prompt_template/services.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
{
//
// Required:
// The displayable name of this node
//
"title": "Prompt Template",
//
// Required:
// The protocol is the endpoint protocol
//
"protocol": "prompt-template://",
//
// Required:
// Class type of the node - what it does
//
"classType": [
"text"
],
//
// Required:
// Capabilities are flags that change the behavior of the underlying
// engine
//
"capabilities": ["experimental"],
//
// Optional:
// Register is either filter, endpoint or ignored if not specified. If the
// type is specified, a factory is registered of that given type
//
"register": "filter",
//
// Optional:
// The node is the actual pyhsical node to instantiate - if
// not specified, the protocol will be used
//
"node": "python",
//
// Optional:
// The path is the executable/script code - it is node dependent
// and is optional for most node
//
"path": "nodes.prompt_template",
//
// Required:
// The prefix map when added/removed when convertting URLs <=> paths
//
"prefix": "prompt_template",
//
// Optional:
// Description to of this driver
//
"description": [
"A transformation component that renders prompt templates with dynamic ",
"variable substitution. Supports {{variable}} placeholders resolved from ",
"pipeline context, built-in helpers ({{now}}, {{uuid}}, {{random}}), ",
"conditional sections ({% if var %}...{% endif %}), loop support ",
"({% for item in list %}...{% endfor %}), and escape sequences for ",
"literal braces. Implemented with stdlib only — no Jinja2 dependency."
],
//
// Optional:
// The icon is the icon to display in the UI for this node
//
"icon": "prompt.svg",
//
// Optional:
// The tile is the displayable name of this node
//
"tile": [],
//
// Optional:
// As a pipe component, define what this pipe component takes
// and what it produces
//
"lanes": {
"text": ["text"],
"questions": ["questions"]
},
"input": [
{
"lane": "text",
"description": "Text input - used as context variables for template rendering",
"output": [
{
"lane": "text",
"description": "Rendered template output as text"
}
]
},
{
"lane": "questions",
"description": "Questions input - question text is rendered through the template",
"output": [
{
"lane": "questions",
"description": "Questions with template-rendered text"
}
]
}
],
//
// Optional:
// Profile section are configuration options used by the driver
// itself
//
"preconfig": {
"default": "default",
"profiles": {
"default": {
"template": "{{input}}",
"variables": {}
}
}
},
//
// Test configuration for automated node testing
//
"test": {
"profiles": ["default"],
"cases": [
{
"questions": {
"questions": [{"text": "Hello world"}]
},
"expect": {
"questions": {
"notEmpty": true
}
}
},
{
"config": {
"template": "Summarize the following: {{input}}",
"variables": {"input": "The quick brown fox jumps over the lazy dog."}
},
"questions": {
"questions": [{"text": "test prompt"}]
},
"expect": {
"questions": {
"notEmpty": true,
"contains": "Summarize the following:"
}
}
},
{
"config": {
"template": "Q={{question}} | I={{input}}",
"variables": {"input": "ctx"}
},
"questions": {
"questions": [{"text": "what is this?"}]
},
"expect": {
"questions": {
"contains": "Q=what is this?"
}
}
}
]
},
//
// Optional:
// Local fields defintions - these define fields only for the
// current service. You may specify them here, or directly
// in the shape
//
"fields": {
"template": {
"type": "string",
"title": "Template",
"description": "Prompt template with {{variable}} placeholders, conditionals ({% if %}), and loops ({% for %})"
},
"variables": {
"type": "object",
"title": "Variables",
"description": "Key-value pairs of variables to substitute into the template"
}
},
//
// Required:
// Defines the fields (shape) of the service. Either source or target
// map be specified, or both, but at least one is required
//
"shape": [
{
"section": "Pipe",
"title": "Template",
"properties": [
"template",
"variables"
]
}
]
}
Loading
Loading