OpenBMB/ChatDev · error · ConfigError

role must be 'user' or 'assistant'

Error message

role must be 'user' or 'assistant'

What it means

LiteralNodeConfig.from_dict rejects an optional 'role' that is not one of the two chat roles 'user' or 'assistant' (compared case-insensitively after strip). Omitting 'role' defaults to USER; only bad explicit values fail.

Solutions

  1. Use only 'user' or 'assistant' for role
  2. Omit 'role' to get the default 'user'
  3. Filter/remap system messages before constructing literal node configs

Example fix

# before
{"content": "hi", "role": "system"}
# after
{"content": "hi", "role": "user"}
Defensive patterns

Strategy: type-guard

Validate before calling

LITERAL_ROLES = {'user', 'assistant'}
role = (data.get('role') or 'user').strip().lower()
if role not in LITERAL_ROLES:
    data['role'] = 'user'

Type guard

def valid_literal_role(data: dict) -> bool:
    r = data.get('role')
    return r is None or (isinstance(r, str) and r.strip().lower() in {'user', 'assistant'})

Try / catch

try:
    LiteralNodeConfig.from_dict(data, path='n1')
except ConfigError as e:
    if e.path.endswith('role'):
        data['role'] = 'user'
        LiteralNodeConfig.from_dict(data, path='n1')
    else:
        raise

Prevention

When it happens

Trigger: Passing role values like 'system', 'tool', 'bot', 'model', or typos like 'asistant'. Matching is lenient about case/whitespace but not about the value itself.

Common situations: Porting message lists from OpenAI-style payloads that include 'system' or 'tool' roles into literal nodes; UI dropdowns exposing unsupported roles.

Understand the failure class

Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.

Related errors


AI-assisted analysis of OpenBMB/ChatDev@4fb2db0ea9 (2026-08-27). Data as JSON: /api/errors/83c83e224980012e. Report an issue: GitHub.

Appendix: source

Thrown at entity/configs/node/literal.py:37

class LiteralNodeConfig(BaseConfig):
    """Config describing the literal payload emitted by the node."""

    content: str = ""
    role: MessageRole = MessageRole.USER

    @classmethod
    def from_dict(cls, data: Mapping[str, Any], *, path: str) -> "LiteralNodeConfig":
        mapping = require_mapping(data, path)
        content = require_str(mapping, "content", path)
        if not content:
            raise ConfigError("content cannot be empty", f"{path}.content")

        role_value = optional_str(mapping, "role", path)
        role = MessageRole.USER
        if role_value:
            normalized = role_value.strip().lower()
            if normalized not in (MessageRole.USER.value, MessageRole.ASSISTANT.value):
                raise ConfigError("role must be 'user' or 'assistant'", f"{path}.role")
            role = MessageRole(normalized)

        return cls(content=content, role=role, path=path)

    def validate(self) -> None:
        if not self.content:
            raise ConfigError("content cannot be empty", f"{self.path}.content")
        if self.role not in (MessageRole.USER, MessageRole.ASSISTANT):
            raise ConfigError("role must be 'user' or 'assistant'", f"{self.path}.role")

    FIELD_SPECS = {
        "content": ConfigFieldSpec(
            name="content",
            display_name="Literal Content",
            type_hint="text",
            required=True,
            description="Plain text emitted whenever the node executes.",
        ),

View on GitHub (pinned to 4fb2db0ea9)