Skip to content

symfonic.core.learning.phases_synthetic

phases_synthetic

Phase 11 -- Synthetic linking (episodic -> semantic cross-pollination).

Scans recent episodic turns for semantic-node co-occurrences, emits candidate edges with provenance='synthetic' for pairs that co-occur

= min_co_count times AND have no existing edge.

build_synthetic_links(
    graph: Any,
    episodic_layer: Any,
    scope: TenantScope,
    *,
    min_co_count: int = DEFAULT_MIN_CO_COUNT,
    init_weight: float = DEFAULT_SYNTHETIC_WEIGHT,
    max_pairs: int = 500,
    gate: DurabilityGate | None = None,
) -> int

Phase 11 -- Scan recent episodic turns for semantic-node co-occurrences, emit candidate edges with provenance='synthetic' for pairs that co-occur

= min_co_count times AND have no existing edge.

Returns the number of synthetic edges created.

Source code in src/symfonic/core/learning/phases_synthetic.py
async def build_synthetic_links(
    graph: Any,
    episodic_layer: Any,
    scope: TenantScope,
    *,
    min_co_count: int = DEFAULT_MIN_CO_COUNT,
    init_weight: float = DEFAULT_SYNTHETIC_WEIGHT,
    max_pairs: int = 500,
    gate: DurabilityGate | None = None,
) -> int:
    """Phase 11 -- Scan recent episodic turns for semantic-node co-occurrences,
    emit candidate edges with provenance='synthetic' for pairs that co-occur
    >= min_co_count times AND have no existing edge.

    Returns the number of synthetic edges created.
    """
    co_map = await _build_cooccurrence_map(
        episodic_layer, scope, DEFAULT_TOP_K_EPISODIC, gate=gate,
    )

    # Resolve labels -> NodeIds in one pass
    try:
        all_nodes = await graph.query_nodes(scope, layer=MemoryLayer.SEMANTIC)
    except Exception:
        logger.debug("Failed to query nodes for synthetic linking", exc_info=True)
        return 0

    label_to_id = {n.label: n.id for n in all_nodes}

    try:
        existing_edges = await graph.list_edges(scope, limit=10_000)
    except Exception:
        existing_edges = []

    edge_set: set[tuple[str, str]] = set()
    for e in existing_edges:
        edge_set.add((str(e.source), str(e.target)))
        edge_set.add((str(e.target), str(e.source)))  # undirected

    from symfonic.memory.models.edge import MemoryEdge

    created = 0
    for (la, lb), episodic_ids in co_map.items():
        if len(episodic_ids) < min_co_count:
            continue
        sid = label_to_id.get(la)
        tid = label_to_id.get(lb)
        if sid is None or tid is None:
            continue
        if (str(sid), str(tid)) in edge_set:
            continue

        edge = MemoryEdge(
            source=sid,
            target=tid,
            relationship="SYNTHETIC_LINK",
            tenant_id=scope.tenant_id,
            weight=init_weight,
            provenance="synthetic",
            source_episodic_ids=episodic_ids[:20],
            uses=0,
        )
        try:
            await graph.add_edge(scope, edge)
        except Exception:
            logger.debug("Failed to create synthetic edge %s->%s", la, lb)
            continue

        created += 1
        if created >= max_pairs:
            break

    return created