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
+1
@@ -0,0 +1 @@
|
||||
from .conditions import *
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
"""Edge condition registry utilities."""
|
||||
|
||||
from .base import EdgeConditionManager, ConditionFactoryContext
|
||||
from .registry import build_edge_condition_manager
|
||||
|
||||
__all__ = [
|
||||
"ConditionFactoryContext",
|
||||
"EdgeConditionManager",
|
||||
"build_edge_condition_manager",
|
||||
]
|
||||
Executable
+233
@@ -0,0 +1,233 @@
|
||||
"""EdgeConditionManager abstraction that unifies edge condition compilation."""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable, Generic, TypeVar
|
||||
|
||||
from entity.messages import Message, MessageRole
|
||||
from runtime.node.executor import ExecutionContext
|
||||
from utils.log_manager import LogManager
|
||||
from utils.structured_logger import get_server_logger
|
||||
from entity.configs.node.node import EdgeLink, Node
|
||||
from entity.configs.edge.edge_condition import EdgeConditionTypeConfig
|
||||
from utils.function_manager import FunctionManager
|
||||
|
||||
ConditionEvaluator = Callable[[str], bool]
|
||||
|
||||
TConfig = TypeVar("TConfig", bound="EdgeConditionTypeConfig")
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ConditionFactoryContext:
|
||||
"""Context passed to managers at initialization time."""
|
||||
|
||||
function_manager: "FunctionManager | None" = None
|
||||
log_manager: "LogManager | None" = None
|
||||
|
||||
|
||||
class EdgeConditionManager(Generic[TConfig], ABC):
|
||||
"""An abstract base class is required, and all edge condition types must implement the process logic."""
|
||||
|
||||
def __init__(self, config: TConfig, ctx: ConditionFactoryContext, execution_context: ExecutionContext) -> None:
|
||||
self.config = config
|
||||
self.ctx = ctx
|
||||
self.execution_context = execution_context
|
||||
|
||||
@abstractmethod
|
||||
def process(
|
||||
self,
|
||||
edge_link: "EdgeLink",
|
||||
source_result: Message,
|
||||
from_node: "Node",
|
||||
log_manager: LogManager,
|
||||
) -> None:
|
||||
"""The execution logic is implemented by the subclasses."""
|
||||
|
||||
def transform_payload(
|
||||
self,
|
||||
payload: Message,
|
||||
*,
|
||||
source_result: Message,
|
||||
from_node: "Node",
|
||||
edge_link: "EdgeLink",
|
||||
log_manager: LogManager,
|
||||
) -> Message | None:
|
||||
processor = edge_link.payload_processor
|
||||
if not processor:
|
||||
return payload
|
||||
try:
|
||||
return processor.transform(
|
||||
payload,
|
||||
source_result=source_result,
|
||||
from_node=from_node,
|
||||
edge_link=edge_link,
|
||||
log_manager=log_manager,
|
||||
context=self.execution_context,
|
||||
)
|
||||
except Exception as exc: # pragma: no cover
|
||||
error_msg = (
|
||||
f"Edge payload processor failed for {from_node.id}->{edge_link.target.id}: {exc}"
|
||||
)
|
||||
if self.ctx and self.ctx.log_manager:
|
||||
self.ctx.log_manager.error(
|
||||
error_msg,
|
||||
details={
|
||||
"processor_type": getattr(edge_link, "process_type", None),
|
||||
"processor_metadata": getattr(edge_link, "process_metadata", {}),
|
||||
},
|
||||
)
|
||||
server_logger = get_server_logger()
|
||||
server_logger.log_exception(
|
||||
exc,
|
||||
error_msg,
|
||||
processor_type=getattr(edge_link, "process_type", None),
|
||||
processor_metadata=getattr(edge_link, "process_metadata", {}),
|
||||
)
|
||||
return payload
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# internal helpers
|
||||
# ------------------------------------------------------------------
|
||||
def _process_with_condition(
|
||||
self,
|
||||
evaluator: ConditionEvaluator,
|
||||
*,
|
||||
label: str,
|
||||
metadata: dict[str, Any],
|
||||
edge_link: "EdgeLink",
|
||||
source_result: Message,
|
||||
from_node: "Node",
|
||||
log_manager: LogManager,
|
||||
) -> None:
|
||||
target_node = edge_link.target
|
||||
log_manager.record_edge_process(from_node.id, target_node.id, edge_link.config)
|
||||
|
||||
serialized_source = self._payload_to_text(source_result)
|
||||
try:
|
||||
condition_met = bool(evaluator(serialized_source))
|
||||
except Exception as exc: # pragma: no cover
|
||||
error_msg = f"Error calling condition '{label}': {exc}"
|
||||
log_manager.error(
|
||||
error_msg,
|
||||
details={
|
||||
"condition_type": edge_link.condition_type,
|
||||
"condition_metadata": metadata,
|
||||
},
|
||||
)
|
||||
server_logger = get_server_logger()
|
||||
server_logger.log_exception(
|
||||
exc,
|
||||
error_msg,
|
||||
condition_type=edge_link.condition_type,
|
||||
condition_metadata=metadata,
|
||||
)
|
||||
condition_met = True
|
||||
|
||||
if not condition_met:
|
||||
log_manager.debug(
|
||||
f"Edge condition not met for {from_node.id} -> {target_node.id}, skipping edge processing"
|
||||
)
|
||||
return
|
||||
|
||||
log_manager.info(f"Edge condition met for {from_node.id} -> {target_node.id}")
|
||||
self._clear_target_context(
|
||||
target_node,
|
||||
drop_non_keep=getattr(edge_link, "clear_context", False),
|
||||
drop_keep=getattr(edge_link, "clear_kept_context", False),
|
||||
from_node=from_node,
|
||||
log_manager=log_manager,
|
||||
)
|
||||
|
||||
if edge_link.carry_data:
|
||||
payload = self._prepare_payload_for_target(source_result, from_node, target_node, edge_link.keep_message)
|
||||
payload = self.transform_payload(
|
||||
payload,
|
||||
source_result=source_result,
|
||||
from_node=from_node,
|
||||
edge_link=edge_link,
|
||||
log_manager=log_manager,
|
||||
)
|
||||
if payload is None:
|
||||
log_manager.debug(
|
||||
f"Payload processor dropped message for edge {from_node.id} -> {target_node.id}"
|
||||
)
|
||||
return
|
||||
|
||||
# Tag message with dynamic edge info for later processing
|
||||
if edge_link.dynamic_config is not None:
|
||||
metadata = dict(payload.metadata)
|
||||
metadata["_from_dynamic_edge"] = True
|
||||
metadata["_dynamic_edge_source"] = from_node.id
|
||||
payload.metadata = metadata
|
||||
|
||||
target_node.append_input(payload)
|
||||
log_manager.debug(
|
||||
f"Data passed from {from_node.id} to {target_node.id}'s input queue "
|
||||
f"(type: {self._describe_payload(payload)})"
|
||||
)
|
||||
else:
|
||||
log_manager.debug(
|
||||
f"Edge {from_node.id} -> {target_node.id} does not carry data, skipping data transfer"
|
||||
)
|
||||
|
||||
if edge_link.trigger:
|
||||
edge_link.triggered = True
|
||||
log_manager.debug(f"Edge {from_node.id} -> {target_node.id} triggered")
|
||||
|
||||
def _payload_to_text(self, payload: Any) -> str:
|
||||
if isinstance(payload, Message):
|
||||
return payload.text_content()
|
||||
if payload is None:
|
||||
return ""
|
||||
return str(payload)
|
||||
|
||||
def _prepare_payload_for_target(
|
||||
self,
|
||||
payload: Any,
|
||||
from_node: Node,
|
||||
target_node: Node,
|
||||
keep: bool = False,
|
||||
) -> Message:
|
||||
if not isinstance(payload, Message):
|
||||
payload = Message(role=MessageRole.ASSISTANT, content=str(payload))
|
||||
cloned = payload.clone()
|
||||
if not cloned.preserve_role:
|
||||
cloned_role = MessageRole.ASSISTANT if from_node.id == target_node.id else MessageRole.USER
|
||||
cloned.role = cloned_role
|
||||
metadata = dict(cloned.metadata)
|
||||
metadata["source"] = from_node.id
|
||||
cloned.metadata = metadata
|
||||
if keep:
|
||||
cloned.keep = True
|
||||
return cloned
|
||||
|
||||
def _clear_target_context(
|
||||
self,
|
||||
target_node: Node,
|
||||
*,
|
||||
drop_non_keep: bool,
|
||||
drop_keep: bool,
|
||||
from_node: Node,
|
||||
log_manager: LogManager,
|
||||
) -> None:
|
||||
if not drop_non_keep and not drop_keep:
|
||||
return
|
||||
removed_non_keep, removed_keep = target_node.clear_inputs_by_flag(
|
||||
drop_non_keep=drop_non_keep,
|
||||
drop_keep=drop_keep,
|
||||
)
|
||||
if removed_non_keep or removed_keep:
|
||||
log_manager.debug(
|
||||
f"Cleared target context for edge {from_node.id} -> {target_node.id}",
|
||||
details={
|
||||
"removed_non_keep": removed_non_keep,
|
||||
"removed_keep": removed_keep,
|
||||
"drop_non_keep": drop_non_keep,
|
||||
"drop_keep": drop_keep,
|
||||
},
|
||||
)
|
||||
|
||||
def _describe_payload(self, payload: Any) -> str:
|
||||
if isinstance(payload, Message):
|
||||
return f"message:{payload.role.value}"
|
||||
return type(payload).__name__
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
"""Register built-in edge condition manager implementations."""
|
||||
from entity.configs.edge.edge_condition import (
|
||||
FunctionEdgeConditionConfig,
|
||||
KeywordEdgeConditionConfig,
|
||||
)
|
||||
from runtime.edge.conditions.registry import register_edge_condition
|
||||
from runtime.edge.conditions.function_manager import FunctionEdgeConditionManager
|
||||
from runtime.edge.conditions.keyword_manager import KeywordEdgeConditionManager
|
||||
|
||||
register_edge_condition(
|
||||
"function",
|
||||
manager_cls=FunctionEdgeConditionManager,
|
||||
summary="Calls registered Python functions from functions/edge (default 'true').",
|
||||
config_cls=FunctionEdgeConditionConfig
|
||||
)
|
||||
register_edge_condition(
|
||||
"keyword",
|
||||
manager_cls=KeywordEdgeConditionManager,
|
||||
summary="Declarative conditions based on include/exclude keywords or regex matching",
|
||||
config_cls=KeywordEdgeConditionConfig
|
||||
)
|
||||
Executable
+67
@@ -0,0 +1,67 @@
|
||||
"""FunctionEdgeManager centralizes function-based edge conditions."""
|
||||
|
||||
from typing import Callable, Optional
|
||||
|
||||
from entity.configs.edge.edge_condition import FunctionEdgeConditionConfig
|
||||
from utils.structured_logger import get_server_logger
|
||||
from .base import ConditionFactoryContext
|
||||
from .base import EdgeConditionManager
|
||||
from utils.function_manager import FunctionManager
|
||||
from ...node.executor import ExecutionContext
|
||||
|
||||
|
||||
class FunctionEdgeConditionManager(EdgeConditionManager[FunctionEdgeConditionConfig]):
|
||||
def __init__(self, config: FunctionEdgeConditionConfig, ctx: ConditionFactoryContext, execution_context: ExecutionContext) -> None:
|
||||
super().__init__(config, ctx, execution_context)
|
||||
self._name = config.name or "true"
|
||||
self.label = self._name or "true"
|
||||
self.metadata = {"function": self._name}
|
||||
self._evaluator = self._build_evaluator()
|
||||
|
||||
def _build_evaluator(self) -> Callable[[str], bool]:
|
||||
if self._name == "true":
|
||||
return lambda _: True
|
||||
|
||||
function_obj = self._resolve_function(self._name, self.ctx.function_manager)
|
||||
if function_obj is None:
|
||||
logger = get_server_logger()
|
||||
logger.warning(f"Edge condition function '{self._name}' not found; defaulting to true")
|
||||
self.metadata = {"function": self._name, "missing": True}
|
||||
|
||||
def missing(_: str) -> bool:
|
||||
if self.ctx.log_manager:
|
||||
self.ctx.log_manager.warning(
|
||||
f"Condition function '{self._name}' not found, defaulting to true"
|
||||
)
|
||||
return True
|
||||
|
||||
return missing
|
||||
|
||||
def evaluator(data: str) -> bool:
|
||||
return bool(function_obj(data))
|
||||
|
||||
return evaluator
|
||||
|
||||
def process(
|
||||
self,
|
||||
edge_link,
|
||||
source_result,
|
||||
from_node,
|
||||
log_manager,
|
||||
) -> None:
|
||||
self._process_with_condition(
|
||||
self._evaluator,
|
||||
label=self.label,
|
||||
metadata=self.metadata,
|
||||
edge_link=edge_link,
|
||||
source_result=source_result,
|
||||
from_node=from_node,
|
||||
log_manager=log_manager,
|
||||
)
|
||||
|
||||
def _resolve_function(
|
||||
self, name: str, function_manager: Optional["FunctionManager"]
|
||||
) -> Optional[Callable[[str], bool]]:
|
||||
if not function_manager:
|
||||
return None
|
||||
return function_manager.get_function(name)
|
||||
Executable
+66
@@ -0,0 +1,66 @@
|
||||
"""KeywordEdgeManager implements declarative keyword conditions."""
|
||||
|
||||
import re
|
||||
|
||||
from entity.configs.edge.edge_condition import KeywordEdgeConditionConfig
|
||||
from .base import ConditionFactoryContext
|
||||
from .base import EdgeConditionManager
|
||||
from ...node.executor import ExecutionContext
|
||||
|
||||
|
||||
class KeywordEdgeConditionManager(EdgeConditionManager[KeywordEdgeConditionConfig]):
|
||||
def __init__(self, config: KeywordEdgeConditionConfig, ctx: ConditionFactoryContext, execution_context: ExecutionContext) -> None:
|
||||
super().__init__(config, ctx, execution_context)
|
||||
self.any_keywords = list(config.any_keywords)
|
||||
self.none_keywords = list(config.none_keywords)
|
||||
self.regex_patterns = list(config.regex_patterns)
|
||||
self.case_sensitive = bool(config.case_sensitive)
|
||||
self.default_value = bool(config.default)
|
||||
lowered_regex_flags = 0 if self.case_sensitive else re.IGNORECASE
|
||||
if self.case_sensitive:
|
||||
self.processed_any = self.any_keywords
|
||||
self.processed_none = self.none_keywords
|
||||
else:
|
||||
self.processed_any = [kw.lower() for kw in self.any_keywords]
|
||||
self.processed_none = [kw.lower() for kw in self.none_keywords]
|
||||
self.compiled_regex = [re.compile(pattern, lowered_regex_flags) for pattern in self.regex_patterns]
|
||||
self.label = f"keyword(any={len(self.any_keywords)}, none={len(self.none_keywords)}, regex={len(self.regex_patterns)})"
|
||||
self.metadata = {
|
||||
"any": self.any_keywords,
|
||||
"none": self.none_keywords,
|
||||
"regex": self.regex_patterns,
|
||||
"case_sensitive": self.case_sensitive,
|
||||
"default": self.default_value,
|
||||
}
|
||||
|
||||
def _evaluate(self, data: str) -> bool:
|
||||
haystack = data if self.case_sensitive else data.lower()
|
||||
for keyword in self.processed_none:
|
||||
if keyword and keyword in haystack:
|
||||
return False
|
||||
for keyword in self.processed_any:
|
||||
if keyword and keyword in haystack:
|
||||
return True
|
||||
for pattern in self.compiled_regex:
|
||||
if pattern.search(data):
|
||||
return True
|
||||
if self.any_keywords or self.regex_patterns:
|
||||
return False
|
||||
return True
|
||||
|
||||
def process(
|
||||
self,
|
||||
edge_link,
|
||||
source_result,
|
||||
from_node,
|
||||
log_manager,
|
||||
) -> None:
|
||||
self._process_with_condition(
|
||||
self._evaluate,
|
||||
label=self.label,
|
||||
metadata=self.metadata,
|
||||
edge_link=edge_link,
|
||||
source_result=source_result,
|
||||
from_node=from_node,
|
||||
log_manager=log_manager,
|
||||
)
|
||||
Executable
+92
@@ -0,0 +1,92 @@
|
||||
"""Registry helpers for pluggable edge condition managers."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from importlib import import_module
|
||||
from typing import Any, Dict, Type
|
||||
|
||||
from schema_registry import register_edge_condition_schema
|
||||
from utils.registry import Registry, RegistryEntry, RegistryError
|
||||
from entity.configs.edge.edge_condition import EdgeConditionConfig, EdgeConditionTypeConfig
|
||||
from .base import EdgeConditionManager, ConditionFactoryContext
|
||||
from ...node.executor import ExecutionContext
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class EdgeConditionRegistration:
|
||||
"""Registry entry describing a pluggable condition type."""
|
||||
|
||||
name: str
|
||||
config_cls: Type["EdgeConditionTypeConfig"]
|
||||
manager_cls: Type["EdgeConditionManager[Any]"]
|
||||
summary: str | None = None
|
||||
|
||||
|
||||
edge_condition_registry = Registry("edge_condition_type")
|
||||
_BUILTINS_LOADED = False
|
||||
|
||||
|
||||
def _ensure_builtins_loaded() -> None:
|
||||
global _BUILTINS_LOADED
|
||||
if not _BUILTINS_LOADED:
|
||||
import_module("runtime.edge.conditions.builtin_types")
|
||||
_BUILTINS_LOADED = True
|
||||
|
||||
|
||||
def register_edge_condition(
|
||||
name: str,
|
||||
*,
|
||||
config_cls: Type["EdgeConditionTypeConfig"],
|
||||
manager_cls: Type["EdgeConditionManager[Any]"],
|
||||
summary: str | None = None,
|
||||
) -> None:
|
||||
"""Register a manager class that encapsulates edge processing logic."""
|
||||
if name in edge_condition_registry.names():
|
||||
raise RegistryError(f"Edge condition type '{name}' already registered")
|
||||
entry = EdgeConditionRegistration(
|
||||
name=name,
|
||||
config_cls=config_cls,
|
||||
manager_cls=manager_cls,
|
||||
summary=summary,
|
||||
)
|
||||
edge_condition_registry.register(name, target=entry)
|
||||
register_edge_condition_schema(name, config_cls=config_cls, summary=summary)
|
||||
|
||||
|
||||
def get_edge_condition_registration(name: str) -> EdgeConditionRegistration:
|
||||
"""Retrieve a registered condition type."""
|
||||
_ensure_builtins_loaded()
|
||||
entry: RegistryEntry = edge_condition_registry.get(name)
|
||||
registration = entry.load()
|
||||
if not isinstance(registration, EdgeConditionRegistration):
|
||||
raise RegistryError(f"Entry '{name}' is not an EdgeConditionRegistration")
|
||||
return registration
|
||||
|
||||
|
||||
def iter_edge_condition_registrations() -> Dict[str, EdgeConditionRegistration]:
|
||||
"""Iterate over registered condition types."""
|
||||
_ensure_builtins_loaded()
|
||||
return {name: entry.load() for name, entry in edge_condition_registry.items()}
|
||||
|
||||
|
||||
def build_edge_condition_manager(
|
||||
condition: "EdgeConditionConfig",
|
||||
context: ConditionFactoryContext,
|
||||
execution_context: ExecutionContext
|
||||
) -> "EdgeConditionManager[Any]":
|
||||
"""Instantiate the manager responsible for a specific edge."""
|
||||
registration = get_edge_condition_registration(condition.type)
|
||||
manager_cls = registration.manager_cls
|
||||
if not manager_cls:
|
||||
raise RegistryError(
|
||||
f"Edge condition type '{condition.type}' does not provide a manager implementation"
|
||||
)
|
||||
return manager_cls(condition.config, context, execution_context)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"EdgeConditionRegistration",
|
||||
"register_edge_condition",
|
||||
"get_edge_condition_registration",
|
||||
"iter_edge_condition_registrations",
|
||||
"build_edge_condition_manager",
|
||||
]
|
||||
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