mirror of
https://github.com/OpenBMB/ChatDev.git
synced 2026-04-25 11:18:06 +00:00
303 lines
11 KiB
Python
Executable File
303 lines
11 KiB
Python
Executable File
"""Edge condition configuration models."""
|
|
|
|
from dataclasses import dataclass, field, fields, replace
|
|
from typing import Any, Dict, Mapping, Type, TypeVar, cast
|
|
|
|
from entity.enum_options import enum_options_from_values
|
|
from schema_registry import (
|
|
SchemaLookupError,
|
|
get_edge_condition_schema,
|
|
iter_edge_condition_schemas,
|
|
)
|
|
|
|
from entity.configs.base import (
|
|
BaseConfig,
|
|
ChildKey,
|
|
ConfigError,
|
|
ConfigFieldSpec,
|
|
ensure_list,
|
|
optional_bool,
|
|
require_mapping,
|
|
require_str,
|
|
extend_path,
|
|
)
|
|
from utils.function_catalog import get_function_catalog
|
|
from utils.function_manager import EDGE_FUNCTION_DIR
|
|
|
|
|
|
def _serialize_config(config: BaseConfig) -> Dict[str, Any]:
|
|
payload: Dict[str, Any] = {}
|
|
for field_obj in fields(config):
|
|
if field_obj.name == "path":
|
|
continue
|
|
payload[field_obj.name] = getattr(config, field_obj.name)
|
|
return payload
|
|
|
|
|
|
class EdgeConditionTypeConfig(BaseConfig):
|
|
"""Base helper for condition-specific configuration classes."""
|
|
|
|
def display_label(self) -> str:
|
|
return self.__class__.__name__
|
|
|
|
def to_external_value(self) -> Any:
|
|
return _serialize_config(self)
|
|
|
|
|
|
@dataclass
|
|
class FunctionEdgeConditionConfig(EdgeConditionTypeConfig):
|
|
"""Configuration for function-based conditions."""
|
|
|
|
name: str = "true"
|
|
|
|
FIELD_SPECS = {
|
|
"name": ConfigFieldSpec(
|
|
name="name",
|
|
display_name="Function Name",
|
|
type_hint="str",
|
|
required=True,
|
|
default="true",
|
|
description="Function Name or 'true' (indicating perpetual satisfaction)",
|
|
)
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Mapping[str, Any] | None, *, path: str) -> "FunctionEdgeConditionConfig":
|
|
if data is None:
|
|
return cls(name="true", path=path)
|
|
mapping = require_mapping(data, path)
|
|
function_name = require_str(mapping, "name", path, allow_empty=False)
|
|
return cls(name=function_name, path=path)
|
|
|
|
@classmethod
|
|
def field_specs(cls) -> Dict[str, ConfigFieldSpec]:
|
|
specs = super().field_specs()
|
|
name_spec = specs.get("name")
|
|
if name_spec is None:
|
|
return specs
|
|
|
|
catalog = get_function_catalog(EDGE_FUNCTION_DIR)
|
|
names = catalog.list_function_names()
|
|
metadata = catalog.list_metadata()
|
|
description = name_spec.description or "Conditional function name"
|
|
if catalog.load_error:
|
|
description = f"{description} (Loading failed: {catalog.load_error})"
|
|
elif not names:
|
|
description = f"{description} (No available conditional functions found)"
|
|
|
|
if "true" not in names:
|
|
names.insert(0, "true")
|
|
descriptions = {"true": "Default condition (always met)"}
|
|
for name in names:
|
|
if name == "true":
|
|
continue
|
|
meta = metadata.get(name)
|
|
descriptions[name] = (meta.description if meta else None) or "The conditional function is not described."
|
|
specs["name"] = replace(
|
|
name_spec,
|
|
enum=names or None,
|
|
enum_options=enum_options_from_values(names, descriptions, preserve_label_case=True),
|
|
description=description,
|
|
)
|
|
return specs
|
|
|
|
def display_label(self) -> str:
|
|
return self.name or "true"
|
|
|
|
def to_external_value(self) -> Any:
|
|
return self.name or "true"
|
|
|
|
|
|
def _normalize_keyword_list(value: Any, path: str) -> list[str]:
|
|
items = ensure_list(value)
|
|
normalized: list[str] = []
|
|
for idx, item in enumerate(items):
|
|
if not isinstance(item, str):
|
|
raise ConfigError("entries must be strings", extend_path(path, f"[{idx}]"))
|
|
normalized.append(item)
|
|
return normalized
|
|
|
|
|
|
@dataclass
|
|
class KeywordEdgeConditionConfig(EdgeConditionTypeConfig):
|
|
"""Configuration for declarative keyword checks."""
|
|
|
|
any_keywords: list[str] = field(default_factory=list)
|
|
none_keywords: list[str] = field(default_factory=list)
|
|
regex_patterns: list[str] = field(default_factory=list)
|
|
case_sensitive: bool = True
|
|
default: bool = False
|
|
|
|
FIELD_SPECS = {
|
|
"any": ConfigFieldSpec(
|
|
name="any",
|
|
display_name="Contains keywords",
|
|
type_hint="list[str]",
|
|
required=False,
|
|
description="Returns True if any keyword is matched.",
|
|
),
|
|
"none": ConfigFieldSpec(
|
|
name="none",
|
|
display_name="Exclude keywords",
|
|
type_hint="list[str]",
|
|
required=False,
|
|
description="If any of the excluded keywords are matched, return False (highest priority).",
|
|
),
|
|
"regex": ConfigFieldSpec(
|
|
name="regex",
|
|
display_name="Regular expressions",
|
|
type_hint="list[str]",
|
|
required=False,
|
|
description="Returns True if any regular expression is matched.",
|
|
advance=True,
|
|
),
|
|
"case_sensitive": ConfigFieldSpec(
|
|
name="case_sensitive",
|
|
display_name="case sensitive",
|
|
type_hint="bool",
|
|
required=False,
|
|
default=True,
|
|
description="Whether to distinguish between uppercase and lowercase letters (default is true).",
|
|
),
|
|
# "default": ConfigFieldSpec(
|
|
# name="default",
|
|
# display_name="Default Result",
|
|
# type_hint="bool",
|
|
# required=False,
|
|
# default=False,
|
|
# description="Return value when no condition matches; defaults to False",
|
|
# advance=True,
|
|
# ),
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Mapping[str, Any], *, path: str) -> "KeywordEdgeConditionConfig":
|
|
mapping = require_mapping(data, path)
|
|
any_keywords = _normalize_keyword_list(mapping.get("any", []), extend_path(path, "any"))
|
|
none_keywords = _normalize_keyword_list(mapping.get("none", []), extend_path(path, "none"))
|
|
regex_patterns = _normalize_keyword_list(mapping.get("regex", []), extend_path(path, "regex"))
|
|
case_sensitive = optional_bool(mapping, "case_sensitive", path, default=True)
|
|
default_value = optional_bool(mapping, "default", path, default=False)
|
|
|
|
if not (any_keywords or none_keywords or regex_patterns):
|
|
raise ConfigError("keyword condition requires any/none/regex", path)
|
|
|
|
return cls(
|
|
any_keywords=any_keywords,
|
|
none_keywords=none_keywords,
|
|
regex_patterns=regex_patterns,
|
|
case_sensitive=True if case_sensitive is None else bool(case_sensitive),
|
|
default=False if default_value is None else bool(default_value),
|
|
path=path,
|
|
)
|
|
|
|
def display_label(self) -> str:
|
|
return f"keyword(any={len(self.any_keywords)}, none={len(self.none_keywords)}, regex={len(self.regex_patterns)})"
|
|
|
|
def to_external_value(self) -> Any:
|
|
payload: Dict[str, Any] = {}
|
|
if self.any_keywords:
|
|
payload["any"] = list(self.any_keywords)
|
|
if self.none_keywords:
|
|
payload["none"] = list(self.none_keywords)
|
|
if self.regex_patterns:
|
|
payload["regex"] = list(self.regex_patterns)
|
|
payload["case_sensitive"] = self.case_sensitive
|
|
payload["default"] = self.default
|
|
return payload
|
|
|
|
|
|
TConditionConfig = TypeVar("TConditionConfig", bound=EdgeConditionTypeConfig)
|
|
|
|
|
|
@dataclass
|
|
class EdgeConditionConfig(BaseConfig):
|
|
"""Wrapper config that stores condition type + concrete config."""
|
|
|
|
type: str
|
|
config: EdgeConditionTypeConfig
|
|
|
|
FIELD_SPECS = {
|
|
"type": ConfigFieldSpec(
|
|
name="type",
|
|
display_name="Condition Type",
|
|
type_hint="str",
|
|
required=True,
|
|
description="Select which condition implementation to run (function, keyword, etc.) so the engine can resolve the schema.",
|
|
),
|
|
"config": ConfigFieldSpec(
|
|
name="config",
|
|
display_name="Condition Config",
|
|
type_hint="object",
|
|
required=True,
|
|
description="Payload interpreted by the chosen function or any/none/regex lists for keyword mode.",
|
|
),
|
|
}
|
|
|
|
@classmethod
|
|
def _normalize_value(cls, value: Any, path: str) -> Mapping[str, Any]:
|
|
if value is None:
|
|
return {"type": "function", "config": {"name": "true"}}
|
|
if isinstance(value, bool):
|
|
if value:
|
|
return {"type": "function", "config": {"name": "true"}}
|
|
return {"type": "function", "config": {"name": "always_false"}}
|
|
if isinstance(value, str):
|
|
return {"type": "function", "config": {"name": value}}
|
|
return require_mapping(value, path)
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Any, *, path: str) -> "EdgeConditionConfig":
|
|
mapping = cls._normalize_value(data, path)
|
|
condition_type = require_str(mapping, "type", path)
|
|
config_payload = mapping.get("config")
|
|
config_path = extend_path(path, "config")
|
|
|
|
try:
|
|
schema = get_edge_condition_schema(condition_type)
|
|
except SchemaLookupError as exc:
|
|
raise ConfigError(f"unknown condition type '{condition_type}'", extend_path(path, "type")) from exc
|
|
if config_payload is None:
|
|
raise ConfigError("condition config is required", config_path)
|
|
condition_config = schema.config_cls.from_dict(config_payload, path=config_path)
|
|
return cls(type=condition_type, config=condition_config, path=path)
|
|
|
|
@classmethod
|
|
def child_routes(cls) -> Dict[ChildKey, Type[BaseConfig]]:
|
|
return {
|
|
ChildKey(field="config", value=name): schema.config_cls
|
|
for name, schema in iter_edge_condition_schemas().items()
|
|
}
|
|
|
|
@classmethod
|
|
def field_specs(cls) -> Dict[str, ConfigFieldSpec]:
|
|
specs = super().field_specs()
|
|
type_spec = specs.get("type")
|
|
if type_spec:
|
|
registrations = iter_edge_condition_schemas()
|
|
names = list(registrations.keys())
|
|
descriptions = {name: schema.summary for name, schema in registrations.items()}
|
|
specs["type"] = replace(
|
|
type_spec,
|
|
enum=names,
|
|
enum_options=enum_options_from_values(names, descriptions, preserve_label_case=True),
|
|
)
|
|
return specs
|
|
|
|
def display_label(self) -> str:
|
|
return self.config.display_label()
|
|
|
|
def to_external_value(self) -> Any:
|
|
if self.type == "function":
|
|
return self.config.to_external_value()
|
|
return {
|
|
"type": self.type,
|
|
"config": self.config.to_external_value(),
|
|
}
|
|
|
|
def as_config(self, expected_type: Type[TConditionConfig]) -> TConditionConfig | None:
|
|
config = self.config
|
|
if isinstance(config, expected_type):
|
|
return cast(TConditionConfig, config)
|
|
return None
|