Skip to content

symfonic.core.learning.entity_extractor

entity_extractor

EntityExtractor protocol + EntityCandidate dataclass + shared utilities.

This module is import-light: no spaCy, no langchain, no I/O. Strategy implementations live in their own files (entity_extractor_spacy.py / entity_extractor_llm.py) so the regex default ships without optional dependencies.

Design reference: .claude/docs/2026-05-23-entity-linker-design.md §4.5, §5, §13.6. Roadmap reference: .claude/docs/2026-05-16-7.1.x-7.2-roadmap.md §11.

EntityCandidate dataclass

EntityCandidate(
    surface_form: str,
    kind: EntityKind,
    confidence: float,
    start: int | None = None,
    end: int | None = None,
)

A single extracted entity mention.

Attributes:

Name Type Description
surface_form str

Verbatim text as observed in the episodic.

kind EntityKind

One of the five canonical entity kinds.

confidence float

Extractor-reported probability in [0.0, 1.0].

start int | None

Optional character offset of the mention in the source text.

end int | None

Optional character offset (exclusive) of the mention.

EntityExtractor

Bases: Protocol

Async pure-function protocol: text -> list[EntityCandidate].

Implementations MUST be side-effect free with respect to the framework state and SHOULD be deterministic so the consolidation phase honours its idempotency contract (design §9). The LLM extractor is the documented exception (design §9.3).

canonicalise_surface

canonicalise_surface(
    surface: str,
    kind: EntityKind,
    *,
    lemmatise: bool = False,
) -> str

Decision 2 canonical-form helper.

Proper-noun buckets (person / place / event) collapse via case-insensitive exact match: the canonical form is the surface lowercased with internal whitespace normalised to single underscores ("Cafe Milano" -> "cafe_milano").

Common-noun buckets (object / other) use the same whitespace normalisation plus lemma+lowercase when lemmatise=True (regex strategy: rule-based; spaCy strategy: real lemmatiser). When lemmatise=False the bucket falls back to lowercase-only.

Returns the empty string when surface is blank.

Source code in src/symfonic/core/learning/entity_extractor.py
def canonicalise_surface(
    surface: str,
    kind: EntityKind,
    *,
    lemmatise: bool = False,
) -> str:
    """Decision 2 canonical-form helper.

    Proper-noun buckets (``person`` / ``place`` / ``event``) collapse via
    case-insensitive exact match: the canonical form is the surface
    lowercased with internal whitespace normalised to single underscores
    ("Cafe Milano" -> "cafe_milano").

    Common-noun buckets (``object`` / ``other``) use the same whitespace
    normalisation plus lemma+lowercase when ``lemmatise=True`` (regex
    strategy: rule-based; spaCy strategy: real lemmatiser). When
    ``lemmatise=False`` the bucket falls back to lowercase-only.

    Returns the empty string when ``surface`` is blank.
    """
    if not surface or not surface.strip():
        return ""
    # Normalise internal whitespace so "Cafe   Milano" and "Cafe Milano"
    # collapse to the same canonical token. Underscore mirrors the
    # existing seed-label convention (``cafe_milano``).
    cleaned = "_".join(surface.split())
    if kind in ("person", "place", "event"):
        return cleaned.lower()
    # Common-noun bucket
    if lemmatise:
        return _lemmatise_common_noun(cleaned)
    return cleaned.lower()

strip_phase11_markers

strip_phase11_markers(text: str) -> str

Remove [semantic:...] and [SOUL: ...] markers from text.

These markers are Phase 11's territory; the entity_links phase must not double-count them as entity surfaces (design §13.6).

Source code in src/symfonic/core/learning/entity_extractor.py
def strip_phase11_markers(text: str) -> str:
    """Remove ``[semantic:...]`` and ``[SOUL: ...]`` markers from text.

    These markers are Phase 11's territory; the entity_links phase
    must not double-count them as entity surfaces (design §13.6).
    """
    if not text:
        return ""
    return _PHASE11_MARKER_RE.sub("", text)