Skip to content

symfonic.capabilities.memory.phases.synthetic

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/capabilities/memory/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}

    bounded = callable(getattr(type(graph), "neighbor_probe", None))
    existing_edges = []
    if not bounded:
        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 bounded:
            for label in (la, lb):
                if label not in label_to_id:
                    matches = await graph.query_nodes(scope, label=label)
                    if matches:
                        label_to_id[label] = matches[0].id
            sid, tid = label_to_id.get(la), label_to_id.get(lb)
        if sid is None or tid is None:
            continue
        if (str(sid), str(tid)) in edge_set:
            continue
        if bounded and await graph.neighbor_probe(scope, sid, target=str(tid)):
            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:
            from symfonic.capabilities.memory.growth import edge_or_defer
            if await edge_or_defer(graph, scope, edge) is None:
                return created
        except Exception:
            logger.debug("Failed to create synthetic edge %s->%s", la, lb)
            continue

        created += 1
        if created >= max_pairs:
            break

    return created