mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-05 05:16:03 +00:00
ruff
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
"""Workflow node base class and decorators"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
@@ -13,35 +14,37 @@ if TYPE_CHECKING:
|
||||
|
||||
class NodePort(pydantic.BaseModel):
|
||||
"""Node port definition"""
|
||||
|
||||
name: str
|
||||
type: str = "any" # any, string, number, boolean, object, array
|
||||
description: str = ""
|
||||
type: str = 'any' # any, string, number, boolean, object, array
|
||||
description: str = ''
|
||||
required: bool = True
|
||||
|
||||
|
||||
class NodeConfig(pydantic.BaseModel):
|
||||
"""Node configuration field definition"""
|
||||
|
||||
name: str
|
||||
type: str # string, integer, number, boolean, select, json, secret, etc.
|
||||
required: bool = False
|
||||
default: Any = None
|
||||
description: str = ""
|
||||
description: str = ''
|
||||
options: Optional[list[str]] = None # For select type
|
||||
|
||||
|
||||
# Validation
|
||||
min_value: Optional[float] = None
|
||||
max_value: Optional[float] = None
|
||||
min_length: Optional[int] = None
|
||||
max_length: Optional[int] = None
|
||||
pattern: Optional[str] = None # Regex pattern
|
||||
|
||||
|
||||
# UI hints
|
||||
placeholder: str = ""
|
||||
placeholder: str = ''
|
||||
show_if: Optional[dict] = None # Conditional display
|
||||
|
||||
|
||||
# Pipeline config source (for reusing Pipeline config metadata)
|
||||
pipeline_config_source: Optional[str] = None # e.g., "pipeline:trigger"
|
||||
|
||||
|
||||
# i18n support for label
|
||||
label: Optional[dict[str, str]] = None # e.g., {"en_US": "Name", "zh_Hans": "名称"}
|
||||
label_zh: Optional[str] = None # Chinese label
|
||||
@@ -50,91 +53,87 @@ class NodeConfig(pydantic.BaseModel):
|
||||
|
||||
class WorkflowNode(abc.ABC):
|
||||
"""Base class for all workflow nodes"""
|
||||
|
||||
|
||||
# Node metadata
|
||||
type_name: str = ""
|
||||
name: str = ""
|
||||
description: str = ""
|
||||
category: str = "misc" # trigger, process, control, action, integration
|
||||
icon: str = ""
|
||||
|
||||
type_name: str = ''
|
||||
name: str = ''
|
||||
description: str = ''
|
||||
category: str = 'misc' # trigger, process, control, action, integration
|
||||
icon: str = ''
|
||||
|
||||
# Port definitions
|
||||
inputs: list[NodePort] = []
|
||||
outputs: list[NodePort] = []
|
||||
|
||||
|
||||
# Configuration schema
|
||||
config_schema: list[NodeConfig] = []
|
||||
|
||||
|
||||
# Pipeline config reuse
|
||||
config_schema_source: Optional[str] = None # e.g., "pipeline:ai"
|
||||
config_stages: list[str] = [] # Specific stages to reuse
|
||||
|
||||
|
||||
def __init__(self, node_id: str, config: dict[str, Any], ap: Optional['app.Application'] = None):
|
||||
"""Initialize node with ID and configuration"""
|
||||
self.node_id = node_id
|
||||
self.config = config
|
||||
self.ap = ap # Reference to the application instance for accessing services
|
||||
|
||||
|
||||
@abc.abstractmethod
|
||||
async def execute(
|
||||
self,
|
||||
inputs: dict[str, Any],
|
||||
context: ExecutionContext
|
||||
) -> dict[str, Any]:
|
||||
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
|
||||
"""
|
||||
Execute the node logic.
|
||||
|
||||
|
||||
Args:
|
||||
inputs: Input data from connected nodes
|
||||
context: Execution context with workflow state
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary of output values
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
async def validate_inputs(self, inputs: dict[str, Any]) -> list[str]:
|
||||
"""
|
||||
Validate input data against port definitions.
|
||||
|
||||
|
||||
Returns:
|
||||
List of validation error messages (empty if valid)
|
||||
"""
|
||||
errors = []
|
||||
for port in self.inputs:
|
||||
if port.required and port.name not in inputs:
|
||||
errors.append(f"Missing required input: {port.name}")
|
||||
errors.append(f'Missing required input: {port.name}')
|
||||
return errors
|
||||
|
||||
|
||||
async def validate_config(self) -> list[str]:
|
||||
"""
|
||||
Validate node configuration.
|
||||
|
||||
|
||||
Returns:
|
||||
List of validation error messages (empty if valid)
|
||||
"""
|
||||
errors = []
|
||||
for cfg in self.config_schema:
|
||||
if cfg.required and cfg.name not in self.config:
|
||||
errors.append(f"Missing required config: {cfg.name}")
|
||||
errors.append(f'Missing required config: {cfg.name}')
|
||||
elif cfg.name in self.config:
|
||||
value = self.config[cfg.name]
|
||||
# Type validation
|
||||
if cfg.type == "integer" and not isinstance(value, int):
|
||||
errors.append(f"Config {cfg.name} must be an integer")
|
||||
elif cfg.type == "number" and not isinstance(value, (int, float)):
|
||||
errors.append(f"Config {cfg.name} must be a number")
|
||||
elif cfg.type == "boolean" and not isinstance(value, bool):
|
||||
errors.append(f"Config {cfg.name} must be a boolean")
|
||||
if cfg.type == 'integer' and not isinstance(value, int):
|
||||
errors.append(f'Config {cfg.name} must be an integer')
|
||||
elif cfg.type == 'number' and not isinstance(value, (int, float)):
|
||||
errors.append(f'Config {cfg.name} must be a number')
|
||||
elif cfg.type == 'boolean' and not isinstance(value, bool):
|
||||
errors.append(f'Config {cfg.name} must be a boolean')
|
||||
# Range validation
|
||||
if cfg.min_value is not None and isinstance(value, (int, float)):
|
||||
if value < cfg.min_value:
|
||||
errors.append(f"Config {cfg.name} must be >= {cfg.min_value}")
|
||||
errors.append(f'Config {cfg.name} must be >= {cfg.min_value}')
|
||||
if cfg.max_value is not None and isinstance(value, (int, float)):
|
||||
if value > cfg.max_value:
|
||||
errors.append(f"Config {cfg.name} must be <= {cfg.max_value}")
|
||||
errors.append(f'Config {cfg.name} must be <= {cfg.max_value}')
|
||||
return errors
|
||||
|
||||
|
||||
# Type mapping from backend to frontend DynamicFormItemType
|
||||
_TYPE_MAP = {
|
||||
'string': 'string',
|
||||
@@ -160,26 +159,26 @@ class WorkflowNode(abc.ABC):
|
||||
def get_config(self, key: str, default: Any = None) -> Any:
|
||||
"""Get configuration value with default"""
|
||||
return self.config.get(key, default)
|
||||
|
||||
|
||||
@classmethod
|
||||
def _config_to_schema_item(cls, cfg: NodeConfig) -> dict[str, Any]:
|
||||
"""Convert a NodeConfig to frontend-compatible schema item"""
|
||||
# Map type to frontend type
|
||||
frontend_type = cls._TYPE_MAP.get(cfg.type, 'string')
|
||||
|
||||
|
||||
# Build i18n label from name
|
||||
label = {
|
||||
'zh_Hans': cfg.name,
|
||||
'en_US': cfg.name,
|
||||
}
|
||||
|
||||
|
||||
# Build i18n description
|
||||
desc = cfg.description or ''
|
||||
description = {
|
||||
'zh_Hans': desc,
|
||||
'en_US': desc,
|
||||
}
|
||||
|
||||
|
||||
result = {
|
||||
'id': cfg.name,
|
||||
'name': cfg.name,
|
||||
@@ -189,11 +188,11 @@ class WorkflowNode(abc.ABC):
|
||||
'required': cfg.required,
|
||||
'default': cfg.default,
|
||||
}
|
||||
|
||||
|
||||
# Add placeholder if present
|
||||
if cfg.placeholder:
|
||||
result['placeholder'] = cfg.placeholder
|
||||
|
||||
|
||||
# Add options if present
|
||||
if cfg.options:
|
||||
result['options'] = [
|
||||
@@ -202,22 +201,22 @@ class WorkflowNode(abc.ABC):
|
||||
'label': {
|
||||
'zh_Hans': opt,
|
||||
'en_US': opt,
|
||||
}
|
||||
},
|
||||
}
|
||||
for opt in cfg.options
|
||||
]
|
||||
|
||||
|
||||
# Add show_if if present
|
||||
if cfg.show_if:
|
||||
result['show_if'] = cfg.show_if
|
||||
|
||||
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def to_schema(cls) -> dict[str, Any]:
|
||||
"""
|
||||
Convert node class to JSON schema for frontend.
|
||||
|
||||
|
||||
Returns:
|
||||
Node schema dictionary
|
||||
"""
|
||||
@@ -235,7 +234,7 @@ class WorkflowNode(abc.ABC):
|
||||
'zh_Hans': desc_zh,
|
||||
'en_US': desc_en,
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
'type': f'{cls.category}.{cls.type_name}',
|
||||
'name': cls.name,
|
||||
@@ -258,16 +257,18 @@ _pending_registrations: list[tuple[str, type[WorkflowNode]]] = []
|
||||
def workflow_node(type_name: str) -> Callable[[type[WorkflowNode]], type[WorkflowNode]]:
|
||||
"""
|
||||
Decorator to register a workflow node type.
|
||||
|
||||
|
||||
Usage:
|
||||
@workflow_node('llm_call')
|
||||
class LLMCallNode(WorkflowNode):
|
||||
...
|
||||
"""
|
||||
|
||||
def decorator(cls: type[WorkflowNode]) -> type[WorkflowNode]:
|
||||
cls.type_name = type_name
|
||||
_pending_registrations.append((type_name, cls))
|
||||
return cls
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user