Skip to content

symfonic.core.learning.phases_entity

phases_entity

Moved to :mod:symfonic.capabilities.memory.phases.entity.

Entity linking is on the Deep Sleep roster, so it lives in the capability that composes that roster and is imported back here for the legacy consolidator to call. One phase, one implementation, for as long as both routes ship.

The private helpers travel with it: the shipped per-phase tests import them by name, and a shim re-exporting only the public surface would have moved the code away from the tests that watch it.

RegexHeuristicExtractor

Decision 4.5 default strategy.

Returns kind='other' and confidence=0.4 for all capitalised tokens (single or whitespace-joined) that pass a small stoplist. The default confidence_threshold=0.5 therefore filters every candidate out by design (§16.0 Q2). Users who want regex extraction must either lower the threshold to 0.4 or switch to the spaCy / LLM extractor.

build_entity_links(graph: Any, episodic_layer: Any, scope: TenantScope, *, extractor: EntityExtractor, min_mention_count: int = DEFAULT_MIN_MENTION_COUNT, max_episodics_per_run: int = DEFAULT_MAX_EPISODICS_PER_RUN, confidence_threshold: float = DEFAULT_CONFIDENCE_THRESHOLD) -> tuple[int, int]

Phase 12.5 implementation.

Returns (entity_nodes_created, mentions_edges_created).

See design §10 for acceptance contract and §9 for idempotency. Per §16.0 Q6, emitted edges are entity-to-entity MENTIONS with co-mention episodic ids on MemoryEdge.source_episodic_ids.

Source code in symfonic/capabilities/memory/phases/entity.py
async def build_entity_links(
    graph: Any,
    episodic_layer: Any,
    scope: TenantScope,
    *,
    extractor: EntityExtractor,
    min_mention_count: int = DEFAULT_MIN_MENTION_COUNT,
    max_episodics_per_run: int = DEFAULT_MAX_EPISODICS_PER_RUN,
    confidence_threshold: float = DEFAULT_CONFIDENCE_THRESHOLD,
) -> tuple[int, int]:
    """Phase 12.5 implementation.

    Returns ``(entity_nodes_created, mentions_edges_created)``.

    See design §10 for acceptance contract and §9 for idempotency.
    Per §16.0 Q6, emitted edges are entity-to-entity ``MENTIONS`` with
    co-mention episodic ids on ``MemoryEdge.source_episodic_ids``.
    """
    # Step 1: scan episodics and collect canonical -> episodic-id list.
    mentions = await scan_episodics(
        episodic_layer, scope, extractor,
        confidence_threshold=confidence_threshold,
        max_episodics=max_episodics_per_run,
    )
    if not mentions:
        return (0, 0)

    # Step 2: apply mention-count gate (deduped episodic-id sets).
    qualified = {
        key: episodic_ids
        for key, episodic_ids in mentions.items()
        if len(set(episodic_ids)) >= min_mention_count
    }
    if not qualified:
        return (0, 0)

    # Step 3: load existing entity nodes and resolve / create.
    existing_entities = await load_existing_entity_nodes(graph, scope)
    extractor_name = type(extractor).__name__
    canonical_to_node: dict[tuple[str, str], MemoryNode] = {}
    nodes_created = 0
    for (canonical, kind), episodic_ids in qualified.items():
        max_conf = max(
            confidence_log_get((canonical, kind)),
            default=0.0,
        )
        node, created = await resolve_or_create_entity_node(
            graph, scope, canonical, kind,  # type: ignore[arg-type]
            episodic_ids=list(set(episodic_ids)),
            extractor_name=extractor_name,
            max_confidence=max_conf,
            existing_entity_nodes=existing_entities,
        )
        if node is None:
            continue
        canonical_to_node[(canonical, kind)] = node
        if created:
            nodes_created += 1
            existing_entities[(canonical, kind)] = node

    # Step 4: emit entity-to-entity MENTIONS edges for co-mentions.
    edges_created = await _emit_mentions_edges(
        graph, scope, qualified, canonical_to_node,
    )
    logger.info(
        "entity_links: tenant=%s nodes_created=%d edges_created=%d "
        "qualified_entities=%d",
        scope.tenant_id, nodes_created, edges_created, len(qualified),
    )
    return (nodes_created, edges_created)