Skip to content

symfonic.agent.fastapi.tenant_privacy_export

tenant_privacy_export

Export-side helpers for the tenant-privacy router.

Split from :mod:tenant_privacy_router to keep each file below the 300-LOC cap. Pure functions; never raise, never touch the audit log.

collect_edges_for_export async

collect_edges_for_export(
    agent: SymfonicAgent, memory_scope: Any
) -> list[dict[str, Any]]

Return every edge owned by the tenant.

The GraphMemoryStore uniquely identifies edges (they live across all layers conceptually), so we hit the semantic layer's graph which is the canonical handle.

Source code in src/symfonic/agent/fastapi/tenant_privacy_export.py
async def collect_edges_for_export(
    agent: SymfonicAgent,
    memory_scope: Any,
) -> list[dict[str, Any]]:
    """Return every edge owned by the tenant.

    The GraphMemoryStore uniquely identifies edges (they live across
    all layers conceptually), so we hit the semantic layer's graph
    which is the canonical handle.
    """
    semantic = agent._orchestrator.get_layer(MemoryLayer.SEMANTIC)
    if semantic is None:
        return []
    graph = getattr(semantic, "_graph", None)
    if graph is None:
        return []
    try:
        edges_raw = await graph.list_edges(memory_scope, limit=100_000)
    except Exception:
        logger.debug("Export: list_edges failed", exc_info=True)
        return []
    return [
        {
            "id": str(e.id),
            "source": str(e.source),
            "target": str(e.target),
            "relationship": e.relationship,
            "weight": e.weight,
            "properties": getattr(e, "properties", {}),
        }
        for e in edges_raw
    ]

collect_nodes_for_export async

collect_nodes_for_export(
    agent: SymfonicAgent, memory_scope: Any
) -> dict[str, list[dict[str, Any]]]

Return tenant nodes grouped by layer value.

Layers that are not registered are silently skipped (empty list entries stay out of the payload rather than implying data exists). Every layer-specific exception is logged but swallowed so a partial outage never short-circuits the rest of the export.

Source code in src/symfonic/agent/fastapi/tenant_privacy_export.py
async def collect_nodes_for_export(
    agent: SymfonicAgent,
    memory_scope: Any,
) -> dict[str, list[dict[str, Any]]]:
    """Return tenant nodes grouped by layer value.

    Layers that are not registered are silently skipped (empty list
    entries stay out of the payload rather than implying data exists).
    Every layer-specific exception is logged but swallowed so a partial
    outage never short-circuits the rest of the export.
    """
    grouped: dict[str, list[dict[str, Any]]] = {}
    for ml in MemoryLayer:
        store = agent._orchestrator.get_layer(ml)
        if store is None:
            continue
        graph = getattr(store, "_graph", None)
        if graph is None:
            # Working layer stores the graph handle under ``_graph_store``.
            graph = getattr(store, "_graph_store", None)
            if graph is None:
                continue
        try:
            # include_retracted=True: GDPR Art.20 portability must return ALL
            # of the user's data, including soft-retracted nodes not yet pruned
            # (they still contain the user's data and are hidden from the
            # default read path).
            nodes = await graph.query_nodes(
                memory_scope, layer=ml, include_retracted=True,
            )
        except Exception:
            logger.debug(
                "Export: failed to query layer %s", ml, exc_info=True,
            )
            continue
        grouped[ml.value] = [
            {
                "id": str(n.id),
                "label": str(n.label),
                "layer": n.layer.value,
                "properties": n.properties,
                "importance": n.importance,
                "access_count": n.access_count,
                "created_at": (
                    str(n.created_at) if n.created_at else None
                ),
                "updated_at": (
                    str(n.updated_at) if n.updated_at else None
                ),
            }
            for n in nodes
        ]
    return grouped