mirror of
https://github.com/OpenBMB/ChatDev.git
synced 2026-04-25 11:18:06 +00:00
140 lines
4.7 KiB
Python
Executable File
140 lines
4.7 KiB
Python
Executable File
"""Schema exporter for dynamic configuration metadata."""
|
|
|
|
import hashlib
|
|
import json
|
|
from dataclasses import dataclass
|
|
from typing import Any, Dict, Iterable, List, Mapping, Sequence, Type
|
|
|
|
from entity.configs import BaseConfig
|
|
from entity.configs.graph import DesignConfig
|
|
|
|
SCHEMA_VERSION = "0.1.0"
|
|
|
|
|
|
class SchemaResolutionError(ValueError):
|
|
"""Raised when breadcrumbs fail to resolve to a config node."""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Breadcrumb:
|
|
"""Describes one hop in the config tree."""
|
|
|
|
node: str
|
|
field: str | None = None
|
|
value: Any | None = None
|
|
|
|
@classmethod
|
|
def from_mapping(cls, data: Mapping[str, Any]) -> "Breadcrumb":
|
|
node = str(data.get("node")) if data.get("node") else ""
|
|
if not node:
|
|
raise SchemaResolutionError("breadcrumb entry missing 'node'")
|
|
field = data.get("field")
|
|
if field is not None:
|
|
field = str(field)
|
|
index = data.get("index")
|
|
if index is not None and not isinstance(index, int):
|
|
raise SchemaResolutionError("breadcrumb 'index' must be integer when provided")
|
|
value = data.get("value")
|
|
return cls(node=node, field=field, value=value)
|
|
|
|
def to_json(self) -> Dict[str, Any]:
|
|
payload: Dict[str, Any] = {"node": self.node}
|
|
if self.field is not None:
|
|
payload["field"] = self.field
|
|
if self.value is not None:
|
|
payload["value"] = self.value
|
|
return payload
|
|
|
|
|
|
def _normalize_breadcrumbs(raw: Sequence[Mapping[str, Any]] | None) -> List[Breadcrumb]:
|
|
if not raw:
|
|
return []
|
|
return [Breadcrumb.from_mapping(item) for item in raw]
|
|
|
|
|
|
def _resolve_config_class(
|
|
breadcrumbs: Sequence[Breadcrumb],
|
|
*,
|
|
root_cls: Type[BaseConfig] = DesignConfig,
|
|
) -> Type[BaseConfig]:
|
|
current_cls: Type[BaseConfig] = root_cls
|
|
for crumb in breadcrumbs:
|
|
if crumb.node != current_cls.__name__:
|
|
raise SchemaResolutionError(
|
|
f"breadcrumb node '{crumb.node}' does not match current config '{current_cls.__name__}'"
|
|
)
|
|
if crumb.field is None:
|
|
continue
|
|
child_cls = current_cls.resolve_child(crumb.field, crumb.value)
|
|
if child_cls is None:
|
|
spec = current_cls.field_specs().get(crumb.field)
|
|
if not spec or spec.child is None:
|
|
raise SchemaResolutionError(
|
|
f"field '{crumb.field}' on {current_cls.__name__} is not navigable"
|
|
)
|
|
child_cls = spec.child
|
|
current_cls = child_cls
|
|
return current_cls
|
|
|
|
|
|
def _serialize_field(config_cls: Type[BaseConfig], name: str, spec_dict: Dict[str, Any]) -> Dict[str, Any]:
|
|
field_spec = spec_dict[name]
|
|
data = field_spec.to_json()
|
|
routes = [
|
|
{
|
|
"childKey": key.to_json(),
|
|
"childNode": target.__name__,
|
|
}
|
|
for key, target in config_cls.child_routes().items()
|
|
if key.field == name
|
|
]
|
|
if routes:
|
|
data["childRoutes"] = routes
|
|
return data
|
|
|
|
|
|
def _ordered_field_names(specs: Mapping[str, Any]) -> List[str]:
|
|
"""Return field names with required ones first while keeping relative order."""
|
|
|
|
items = list(specs.items())
|
|
required_names = [name for name, spec in items if getattr(spec, "required", False)]
|
|
optional_names = [name for name, spec in items if not getattr(spec, "required", False)]
|
|
return required_names + optional_names
|
|
|
|
|
|
def _hash_payload(payload: Dict[str, Any]) -> str:
|
|
serialized = json.dumps(payload, sort_keys=True, ensure_ascii=False, default=str)
|
|
return hashlib.sha1(serialized.encode("utf-8")).hexdigest()
|
|
|
|
|
|
def build_schema_response(
|
|
breadcrumbs_raw: Sequence[Mapping[str, Any]] | None = None,
|
|
*,
|
|
root_cls: Type[BaseConfig] = DesignConfig,
|
|
) -> Dict[str, Any]:
|
|
"""Return a JSON-serializable schema response for the provided breadcrumbs."""
|
|
|
|
breadcrumbs = _normalize_breadcrumbs(breadcrumbs_raw)
|
|
target_cls = _resolve_config_class(breadcrumbs, root_cls=root_cls)
|
|
schema_node = target_cls.collect_schema()
|
|
field_specs = target_cls.field_specs()
|
|
ordered_fields = _ordered_field_names(field_specs)
|
|
fields_payload = [_serialize_field(target_cls, name, field_specs) for name in ordered_fields]
|
|
|
|
response = {
|
|
"schemaVersion": SCHEMA_VERSION,
|
|
"node": schema_node.node,
|
|
"fields": fields_payload,
|
|
"constraints": [constraint.to_json() for constraint in schema_node.constraints],
|
|
"breadcrumbs": [crumb.to_json() for crumb in breadcrumbs],
|
|
}
|
|
response["cacheKey"] = _hash_payload({"node": schema_node.node, "breadcrumbs": response["breadcrumbs"]})
|
|
return response
|
|
|
|
|
|
__all__ = [
|
|
"Breadcrumb",
|
|
"SchemaResolutionError",
|
|
"build_schema_response",
|
|
]
|