chore: import upstream snapshot with attribution
Validate YAML Workflows / Validate YAML Configuration Files (push) Has been cancelled
Validate YAML Workflows / Validate YAML Configuration Files (push) Has been cancelled
This commit is contained in:
Executable
+10
@@ -0,0 +1,10 @@
|
||||
"""Public helpers for edge payload processors."""
|
||||
|
||||
from .base import EdgePayloadProcessor, ProcessorFactoryContext
|
||||
from .registry import build_edge_processor
|
||||
|
||||
__all__ = [
|
||||
"EdgePayloadProcessor",
|
||||
"ProcessorFactoryContext",
|
||||
"build_edge_processor",
|
||||
]
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
"""Base classes for payload processors applied on edges."""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Generic, TypeVar
|
||||
|
||||
from entity.messages import Message
|
||||
from runtime.node.executor import ExecutionContext
|
||||
from utils.log_manager import LogManager
|
||||
from utils.function_manager import FunctionManager
|
||||
|
||||
TConfig = TypeVar("TConfig")
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ProcessorFactoryContext:
|
||||
"""Context passed to processor implementations."""
|
||||
|
||||
function_manager: "FunctionManager | None" = None
|
||||
log_manager: "LogManager | None" = None
|
||||
|
||||
|
||||
class EdgePayloadProcessor(Generic[TConfig], ABC):
|
||||
"""Base payload processor API."""
|
||||
|
||||
label: str | None = None
|
||||
metadata: dict[str, Any] | None = None
|
||||
|
||||
def __init__(self, config: TConfig, ctx: ProcessorFactoryContext) -> None:
|
||||
self.config = config
|
||||
self.ctx = ctx
|
||||
|
||||
@abstractmethod
|
||||
def transform(
|
||||
self,
|
||||
payload: Message,
|
||||
*,
|
||||
source_result: Message,
|
||||
from_node: Any,
|
||||
edge_link: Any,
|
||||
log_manager: LogManager,
|
||||
context: ExecutionContext,
|
||||
) -> Message | None:
|
||||
"""Return transformed payload or None to drop the message."""
|
||||
|
||||
def _clone(self, payload: Message) -> Message:
|
||||
return payload.clone()
|
||||
|
||||
def _text(self, payload: Message) -> str:
|
||||
return payload.text_content()
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
"""Register built-in edge payload processors."""
|
||||
|
||||
from entity.configs.edge.edge_processor import (
|
||||
RegexEdgeProcessorConfig,
|
||||
FunctionEdgeProcessorConfig,
|
||||
)
|
||||
from .registry import register_edge_processor
|
||||
from .regex_processor import RegexEdgePayloadProcessor
|
||||
from .function_processor import FunctionEdgePayloadProcessor
|
||||
|
||||
register_edge_processor(
|
||||
"regex_extract",
|
||||
processor_cls=RegexEdgePayloadProcessor,
|
||||
summary="Extract payload fragments via Python regular expressions.",
|
||||
config_cls=RegexEdgeProcessorConfig,
|
||||
)
|
||||
|
||||
register_edge_processor(
|
||||
"function",
|
||||
processor_cls=FunctionEdgePayloadProcessor,
|
||||
summary="Delegate message transformation to Python functions in functions/edge_processor.",
|
||||
config_cls=FunctionEdgeProcessorConfig,
|
||||
)
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
"""Payload processor that delegates to Python functions."""
|
||||
|
||||
from typing import Any, Callable, Mapping
|
||||
|
||||
from entity.messages import Message
|
||||
from utils.log_manager import LogManager
|
||||
from utils.structured_logger import get_server_logger
|
||||
from .base import EdgePayloadProcessor, ProcessorFactoryContext
|
||||
from entity.configs.edge.edge_processor import FunctionEdgeProcessorConfig
|
||||
from runtime.node.executor import ExecutionContext
|
||||
|
||||
|
||||
class FunctionEdgePayloadProcessor(EdgePayloadProcessor[FunctionEdgeProcessorConfig]):
|
||||
def __init__(self, config: FunctionEdgeProcessorConfig, ctx: ProcessorFactoryContext) -> None:
|
||||
super().__init__(config, ctx)
|
||||
self._name = config.name
|
||||
self.label = f"function({self._name})"
|
||||
self.metadata = {"function": self._name}
|
||||
self._callable = self._resolve()
|
||||
|
||||
def transform(
|
||||
self,
|
||||
payload: Message,
|
||||
*,
|
||||
source_result: Message,
|
||||
from_node,
|
||||
edge_link,
|
||||
log_manager: LogManager,
|
||||
context: ExecutionContext,
|
||||
) -> Message | None:
|
||||
if self._callable is None:
|
||||
log_manager.warning(
|
||||
f"Processor function '{self._name}' not found. Falling back to passthrough."
|
||||
)
|
||||
return payload
|
||||
try:
|
||||
result = self._callable(payload.text_content(), context.global_state)
|
||||
except Exception as exc: # pragma: no cover - defensive logging
|
||||
error_msg = f"Processor function '{self._name}' failed: {exc}"
|
||||
log_manager.error(error_msg)
|
||||
server_logger = get_server_logger()
|
||||
server_logger.log_exception(exc, error_msg, processor_name=self._name)
|
||||
return payload
|
||||
|
||||
if result is None:
|
||||
return None
|
||||
|
||||
return self._coerce_result(payload, result)
|
||||
|
||||
def _resolve(self) -> Callable[[str, dict[str, Any]], Any] | None:
|
||||
manager = self.ctx.function_manager if self.ctx else None
|
||||
if not manager:
|
||||
return None
|
||||
func = manager.get_function(self._name)
|
||||
if func is None:
|
||||
return None
|
||||
return func
|
||||
|
||||
def _coerce_result(self, payload: Message, result: Any) -> Message | None:
|
||||
if isinstance(result, Message):
|
||||
return result
|
||||
cloned = payload.clone()
|
||||
if isinstance(result, str):
|
||||
cloned.content = result
|
||||
return cloned
|
||||
if isinstance(result, Mapping):
|
||||
if result.get("drop"):
|
||||
return None
|
||||
if "content" in result:
|
||||
cloned.content = result["content"]
|
||||
metadata = dict(cloned.metadata)
|
||||
metadata.update(result.get("metadata") or {})
|
||||
cloned.metadata = metadata
|
||||
return cloned
|
||||
cloned.content = str(result)
|
||||
return cloned
|
||||
Executable
+92
@@ -0,0 +1,92 @@
|
||||
"""Regex-based payload processor."""
|
||||
|
||||
import re
|
||||
from typing import List
|
||||
|
||||
from entity.messages import Message
|
||||
from utils.log_manager import LogManager
|
||||
from .base import EdgePayloadProcessor, ProcessorFactoryContext
|
||||
from entity.configs.edge.edge_processor import RegexEdgeProcessorConfig
|
||||
from runtime.node.executor import ExecutionContext
|
||||
|
||||
|
||||
class RegexEdgePayloadProcessor(EdgePayloadProcessor[RegexEdgeProcessorConfig]):
|
||||
def __init__(self, config: RegexEdgeProcessorConfig, ctx: ProcessorFactoryContext) -> None:
|
||||
super().__init__(config, ctx)
|
||||
flags = 0
|
||||
if not config.case_sensitive:
|
||||
flags |= re.IGNORECASE
|
||||
if config.multiline:
|
||||
flags |= re.MULTILINE
|
||||
if config.dotall:
|
||||
flags |= re.DOTALL
|
||||
self._pattern = re.compile(config.pattern, flags)
|
||||
self.label = f"regex({config.pattern})"
|
||||
self.metadata = {
|
||||
"pattern": config.pattern,
|
||||
"group": config.group,
|
||||
"multiple": config.multiple,
|
||||
}
|
||||
|
||||
def transform(
|
||||
self,
|
||||
payload: Message,
|
||||
*,
|
||||
source_result: Message,
|
||||
from_node,
|
||||
edge_link,
|
||||
log_manager: LogManager,
|
||||
context: ExecutionContext,
|
||||
) -> Message | None:
|
||||
matches = list(self._pattern.finditer(self._text(payload)))
|
||||
if not matches:
|
||||
return self._handle_no_match(payload, log_manager, from_node.id, edge_link.target.id)
|
||||
|
||||
extracted = self._extract_values(matches)
|
||||
if not extracted:
|
||||
return self._handle_no_match(payload, log_manager, from_node.id, edge_link.target.id)
|
||||
|
||||
cloned = payload.clone()
|
||||
if self.config.multiple:
|
||||
cloned.content = "\n".join(extracted)
|
||||
else:
|
||||
cloned.content = extracted[0]
|
||||
return cloned
|
||||
|
||||
def _extract_values(self, matches: List[re.Match]) -> List[str]:
|
||||
values: List[str] = []
|
||||
for match in matches:
|
||||
group = self.config.group
|
||||
if group is None:
|
||||
value = match.group(0)
|
||||
else:
|
||||
value = match.group(group)
|
||||
if value is None:
|
||||
continue
|
||||
if self.config.template:
|
||||
value = self.config.template.replace("{match}", value)
|
||||
values.append(value)
|
||||
if not self.config.multiple:
|
||||
break
|
||||
return values
|
||||
|
||||
def _handle_no_match(
|
||||
self,
|
||||
payload: Message,
|
||||
log_manager: LogManager,
|
||||
from_node_id: str,
|
||||
to_node_id: str,
|
||||
) -> Message | None:
|
||||
behavior = self.config.on_no_match
|
||||
if behavior == "drop":
|
||||
log_manager.debug(
|
||||
f"Regex processor dropped payload for {from_node_id}->{to_node_id} (no match)"
|
||||
)
|
||||
return None
|
||||
if behavior == "default":
|
||||
default_value = self.config.default_value or ""
|
||||
cloned = payload.clone()
|
||||
cloned.content = default_value
|
||||
return cloned
|
||||
return payload
|
||||
|
||||
Executable
+81
@@ -0,0 +1,81 @@
|
||||
"""Registry helpers for edge payload processors."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from importlib import import_module
|
||||
from typing import Any, Dict, Type
|
||||
|
||||
from schema_registry import register_edge_processor_schema
|
||||
from utils.registry import Registry, RegistryEntry, RegistryError
|
||||
from entity.configs.edge.edge_processor import EdgeProcessorConfig, EdgeProcessorTypeConfig
|
||||
from .base import EdgePayloadProcessor, ProcessorFactoryContext
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class EdgeProcessorRegistration:
|
||||
name: str
|
||||
config_cls: Type["EdgeProcessorTypeConfig"]
|
||||
processor_cls: Type["EdgePayloadProcessor[Any]"]
|
||||
summary: str | None = None
|
||||
|
||||
|
||||
edge_processor_registry = Registry("edge_processor_type")
|
||||
_BUILTINS_LOADED = False
|
||||
|
||||
|
||||
def _ensure_builtins_loaded() -> None:
|
||||
global _BUILTINS_LOADED
|
||||
if not _BUILTINS_LOADED:
|
||||
import_module("runtime.edge.processors.builtin_types")
|
||||
_BUILTINS_LOADED = True
|
||||
|
||||
|
||||
def register_edge_processor(
|
||||
name: str,
|
||||
*,
|
||||
config_cls: Type["EdgeProcessorTypeConfig"],
|
||||
processor_cls: Type["EdgePayloadProcessor[Any]"],
|
||||
summary: str | None = None,
|
||||
) -> None:
|
||||
if name in edge_processor_registry.names():
|
||||
raise RegistryError(f"Edge processor type '{name}' already registered")
|
||||
entry = EdgeProcessorRegistration(
|
||||
name=name,
|
||||
config_cls=config_cls,
|
||||
processor_cls=processor_cls,
|
||||
summary=summary,
|
||||
)
|
||||
edge_processor_registry.register(name, target=entry)
|
||||
register_edge_processor_schema(name, config_cls=config_cls, summary=summary)
|
||||
|
||||
|
||||
def get_edge_processor_registration(name: str) -> EdgeProcessorRegistration:
|
||||
_ensure_builtins_loaded()
|
||||
entry: RegistryEntry = edge_processor_registry.get(name)
|
||||
registration = entry.load()
|
||||
if not isinstance(registration, EdgeProcessorRegistration):
|
||||
raise RegistryError(f"Entry '{name}' is not an EdgeProcessorRegistration")
|
||||
return registration
|
||||
|
||||
|
||||
def iter_edge_processor_registrations() -> Dict[str, EdgeProcessorRegistration]:
|
||||
_ensure_builtins_loaded()
|
||||
return {name: entry.load() for name, entry in edge_processor_registry.items()}
|
||||
|
||||
|
||||
def build_edge_processor(
|
||||
processor_config: "EdgeProcessorConfig",
|
||||
context: ProcessorFactoryContext,
|
||||
) -> "EdgePayloadProcessor[Any]":
|
||||
registration = get_edge_processor_registration(processor_config.type)
|
||||
processor_cls = registration.processor_cls
|
||||
if not processor_cls:
|
||||
raise RegistryError(f"Edge processor type '{processor_config.type}' does not provide an implementation")
|
||||
return processor_cls(processor_config.config, context)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"register_edge_processor",
|
||||
"get_edge_processor_registration",
|
||||
"iter_edge_processor_registrations",
|
||||
"build_edge_processor",
|
||||
]
|
||||
Reference in New Issue
Block a user