Skip to content

Appraisal Schema

The appraisal_schema module provides a pluggable appraisal-theory mechanism. Instead of hard-coding Scherer's 5 SECs everywhere, you define an AppraisalSchema that bundles dimension definitions, an LLM prompt, and a projection function.

Classes

emotional_memory.appraisal_schema.AppraisalDimension

Bases: BaseModel

One axis of an AppraisalSchema.

Attributes:

Name Type Description
name str

Machine-readable identifier (used as JSON key and dict key).

range tuple[float, float]

(lo, hi) — the valid numeric interval for this dimension.

neutral float

Value representing the neutral / resting point.

description str

Human-readable description forwarded to the LLM prompt.

emotional_memory.appraisal_schema.AppraisalSchema(name: str, dimensions: tuple[AppraisalDimension, ...], system_prompt: str, project_to_core_affect: Callable[[Mapping[str, float]], CoreAffect])

A complete appraisal theory schema.

Bundles the dimension definitions, the LLM system prompt, and the projection function that maps a dimension dict to CoreAffect.

Attributes:

Name Type Description
name

Short identifier string (e.g. "scherer_cpm").

dimensions

Ordered tuple of AppraisalDimension objects.

system_prompt

System prompt to send to the LLM.

project_to_core_affect

Callable mapping {dim_name: float} → CoreAffect.

Source code in src/emotional_memory/appraisal_schema.py
def __init__(
    self,
    name: str,
    dimensions: tuple[AppraisalDimension, ...],
    system_prompt: str,
    project_to_core_affect: Callable[[Mapping[str, float]], CoreAffect],
) -> None:
    self.name = name
    self.dimensions = dimensions
    self.system_prompt = system_prompt
    self.project_to_core_affect = project_to_core_affect

to_json_schema() -> dict[str, Any]

Derive a JSON schema dict from self.dimensions for LLM structured output.

Source code in src/emotional_memory/appraisal_schema.py
def to_json_schema(self) -> dict[str, Any]:
    """Derive a JSON schema dict from ``self.dimensions`` for LLM structured output."""
    props: dict[str, Any] = {}
    for dim in self.dimensions:
        lo, hi = dim.range
        props[dim.name] = {
            "type": "number",
            "minimum": lo,
            "maximum": hi,
            "description": dim.description,
        }
    return {
        "type": "object",
        "properties": props,
        "required": [d.name for d in self.dimensions],
        "additionalProperties": False,
    }

emotional_memory.appraisal.GenericAppraisalVector(dimensions: Mapping[str, object], schema: AppraisalSchema)

An appraisal vector for non-Scherer schemas.

Produced by LLMAppraisalEngine when a custom AppraisalSchema is configured. The Scherer-specific field names are not meaningful here; call to_core_affect() to project into valence/arousal space via the schema's mapping function.

Source code in src/emotional_memory/appraisal.py
def __init__(self, dimensions: Mapping[str, object], schema: AppraisalSchema) -> None:
    # Read-only view: the same instance is shared from the LLM engine's LRU
    # cache, so a caller mutating ``dimensions`` would corrupt every consumer.
    self.dimensions: Mapping[str, float] = MappingProxyType(
        coerce_appraisal_dimensions(dimensions, schema)
    )
    self._schema: AppraisalSchema = schema

Built-in schemas

emotional_memory.appraisal_schema.SCHERER_CPM_SCHEMA: AppraisalSchema = AppraisalSchema(name='scherer_cpm', dimensions=(AppraisalDimension(name='novelty', range=(-1.0, 1.0), neutral=0.0, description='How unexpected: -1=fully expected, 0=neutral, 1=totally new'), AppraisalDimension(name='goal_relevance', range=(-1.0, 1.0), neutral=0.0, description='Relation to goals: -1=obstructs, 0=irrelevant, 1=furthers'), AppraisalDimension(name='coping_potential', range=(0.0, 1.0), neutral=0.5, description='Perceived ability to handle: 0=helpless, 1=full control'), AppraisalDimension(name='norm_congruence', range=(-1.0, 1.0), neutral=0.0, description='Alignment with norms/values: -1=violates, 0=neutral, 1=conforms'), AppraisalDimension(name='self_relevance', range=(0.0, 1.0), neutral=0.0, description='Personal significance: 0=irrelevant, 1=deeply personal')), system_prompt=_SCHERER_SYSTEM_PROMPT, project_to_core_affect=_scherer_project) module-attribute

emotional_memory.appraisal_schema.DIRECT_VAD_SCHEMA: AppraisalSchema = AppraisalSchema(name='direct_vad', dimensions=(AppraisalDimension(name='valence', range=(-1.0, 1.0), neutral=0.0, description='Pleasantness: -1=unpleasant/negative, 0=neutral, 1=pleasant/positive'), AppraisalDimension(name='arousal', range=(0.0, 1.0), neutral=0.5, description='Activation: 0=calm/subdued, 1=excited/activated'), AppraisalDimension(name='dominance', range=(0.0, 1.0), neutral=0.5, description='Control: 0=controlled/submissive, 1=in-control/dominant')), system_prompt=_DIRECT_VAD_SYSTEM_PROMPT, project_to_core_affect=lambda d: CoreAffect(valence=d['valence'], arousal=d['arousal'], dominance=d['dominance'])) module-attribute

Direct valence/arousal/dominance appraisal — the LLM rates V/A/D directly (identity projection) instead of the 5 Scherer SECs.

Opt-in alternative to the default SCHERER_CPM_SCHEMA. Against human-annotated affect (EmoBank, N=300) it is better correlated on every axis — valence r=0.79 (near-zero bias), arousal r=0.58, dominance r=0.43 — vs the SEC->projection's 0.70 / 0.23 / 0.31 (Addendum V, benchmarks/preregistration_addendum_v_direct_vad_closure.md). Trade-offs: it yields a GenericAppraisalVector (no Scherer SEC fields), so the dual-path elaborate() SEC-appraisal storage and any SEC-reading feature require the default schema; and its arousal absolute scale is less calibrated (higher MAE) than its correlation. Use when you only need CoreAffect and want stronger human-gold agreement. Select via LLMAppraisalConfig(appraisal_schema=DIRECT_VAD_SCHEMA).

Defining a custom schema

from emotional_memory import (
    AppraisalDimension,
    AppraisalSchema,
    LLMAppraisalConfig,
    LLMAppraisalEngine,
)
from emotional_memory.affect import CoreAffect

occ_schema = AppraisalSchema(
    name="occ_subset",
    dimensions=(
        AppraisalDimension(
            name="desirability",
            range=(-1.0, 1.0),
            neutral=0.0,
            description="Desired outcome: -1=bad, 1=good",
        ),
        AppraisalDimension(
            name="praiseworthiness",
            range=(-1.0, 1.0),
            neutral=0.0,
            description="Agent's action quality: -1=blame, 1=praise",
        ),
    ),
    system_prompt=(
        "Evaluate the event on OCC dimensions: desirability and praiseworthiness. "
        "Return ONLY a JSON object with those two keys."
    ),
    project_to_core_affect=lambda d: CoreAffect(
        valence=0.6 * d["desirability"] + 0.4 * d["praiseworthiness"],
        arousal=0.5 * abs(d["desirability"]),
        dominance=0.5,
    ),
)

engine = LLMAppraisalEngine(
    llm=my_llm,
    config=LLMAppraisalConfig(appraisal_schema=occ_schema),
)
result = engine.appraise("She won the competition against all odds.")
# result is GenericAppraisalVector(schema_name="occ_subset", ...)
core = result.to_core_affect()

The project_to_core_affect callable receives a Mapping[str, float] where each key is a dimension name and each value is the LLM's rating, clamped to dim.range. It must return a CoreAffect instance.

Back-compatibility

LLMAppraisalEngine() without an explicit appraisal_schema continues to use SCHERER_CPM_SCHEMA and returns AppraisalVector objects as before. All existing code that constructs AppraisalVector directly or reads its named fields is unaffected.