Skip to content

symfonic.platform.transport_episodic_create

transport_episodic_create

Native HTTP adapter for episodic memory creation.

EpisodicCreateRequest

Bases: BaseModel

The existing strict episodic-create request shape.

mount_episodic_create_route

mount_episodic_create_route(router: Any, *, service: Any, resolve: Any, verified_resolver: bool) -> None

Keep the live POST contract even when no episodic store is composed.

Source code in src/symfonic/platform/transport_episodic_create.py
def mount_episodic_create_route(
    router: Any, *, service: Any, resolve: Any, verified_resolver: bool,
) -> None:
    """Keep the live POST contract even when no episodic store is composed."""

    from fastapi import Depends, Header, HTTPException, Request

    async def resolved_scope(
        request: Request,
        x_tenant_id: str | None = Header(default=None),  # noqa: B008
        authorization: str | None = Header(default=None),  # noqa: B008
    ) -> tuple[Any, Any]:
        caller = (
            await resolve(x_tenant_id, authorization, request=request, principal_only=True)
            if verified_resolver else None
        )
        scope = caller.scope if caller is not None else await resolve(
            x_tenant_id, authorization, request=request,
        )
        return scope, caller

    @router.post(
        "/memories/episodic", status_code=201, response_model=EpisodicCreateResponse,
    )
    async def create_episodic(
        body: EpisodicCreateRequest,
        request: Request,
        resolved: tuple[Any, Any] = Depends(resolved_scope),  # noqa: B008
    ) -> dict[str, Any]:
        scope, caller = resolved
        if service is None:
            raise HTTPException(status_code=404, detail="episodic layer not available")
        row = await service.create(
            memory_scope(scope), content=body.content, action_type=body.action_type,
            importance=body.importance, metadata=body.metadata,
            source_phase=body.source_phase,
        )
        logger = get_audit_logger()
        if logger is not None:
            from symfonic.agent.fastapi.identity import request_user_id

            user_id = caller.principal_id if caller is not None else request_user_id(request)
            entry = AuditLogEntry(
                tenant_id=str(scope.tenant_id),
                user_id=str(user_id) if user_id else None,
                action="memory.create.episodic", resource_type="memory_node",
                resource_id=row["id"],
                metadata={"action_type": body.action_type} if body.action_type else {},
                ip_address=request.client.host if request.client else None,
                user_agent=request.headers.get("user-agent"),
            )
            try:
                await logger.record(entry)
            except Exception:  # noqa: BLE001 - legacy audit is best effort
                import logging

                logging.getLogger(__name__).warning(
                    "Audit logger record() raised — audit event lost (action=%s)",
                    entry.action,
                )
        return row