systemprompt-api 0.2.0

HTTP API server and gateway for systemprompt.io OS
Documentation
use axum::extract::{Path, Query, State};
use axum::http::{StatusCode, header};
use axum::response::{IntoResponse, Response};
use axum::{Extension, Json};
use serde::Deserialize;
use systemprompt_models::api::ApiError;

use systemprompt_agent::repository::content::ArtifactRepository;
use systemprompt_identifiers::{ArtifactId, ContextId, TaskId, UserId};
use systemprompt_mcp::services::ui_renderer::MCP_APP_MIME_TYPE;
use systemprompt_mcp::services::ui_renderer::registry::create_default_registry;
use systemprompt_models::RequestContext;
use systemprompt_runtime::AppContext;

#[derive(Debug, Clone, Copy, Deserialize)]
pub struct ArtifactQueryParams {
    pub limit: Option<u32>,
}

pub async fn list_artifacts_by_context(
    Extension(_req_ctx): Extension<RequestContext>,
    State(app_context): State<AppContext>,
    Path(context_id): Path<String>,
) -> Result<impl IntoResponse, ApiError> {
    tracing::debug!(context_id = %context_id, "Listing artifacts by context");

    let artifact_repo = ArtifactRepository::new(app_context.db_pool())
        .map_err(|e| ApiError::internal_error(format!("Database error: {e}")))?;

    let context_id_typed = ContextId::new(&context_id);
    let artifacts = artifact_repo
        .get_artifacts_by_context(&context_id_typed)
        .await
        .map_err(|e| {
            tracing::error!(error = %e, "Failed to list artifacts");
            ApiError::internal_error("Failed to retrieve artifacts")
        })?;

    tracing::debug!(
        context_id = %context_id,
        count = artifacts.len(),
        "Artifacts listed"
    );
    Ok((StatusCode::OK, Json(artifacts)))
}

pub async fn list_artifacts_by_task(
    Extension(_req_ctx): Extension<RequestContext>,
    State(app_context): State<AppContext>,
    Path(task_id): Path<String>,
) -> Result<impl IntoResponse, ApiError> {
    tracing::debug!(task_id = %task_id, "Listing artifacts by task");

    let artifact_repo = ArtifactRepository::new(app_context.db_pool())
        .map_err(|e| ApiError::internal_error(format!("Database error: {e}")))?;

    let task_id_typed = TaskId::new(&task_id);
    let artifacts = artifact_repo
        .get_artifacts_by_task(&task_id_typed)
        .await
        .map_err(|e| {
            tracing::error!(error = %e, "Failed to list artifacts");
            ApiError::internal_error("Failed to retrieve artifacts")
        })?;

    tracing::debug!(
        task_id = %task_id,
        count = artifacts.len(),
        "Artifacts listed"
    );
    Ok((StatusCode::OK, Json(artifacts)))
}

pub async fn get_artifact(
    Extension(_req_ctx): Extension<RequestContext>,
    State(app_context): State<AppContext>,
    Path(artifact_id): Path<String>,
) -> Result<impl IntoResponse, ApiError> {
    tracing::debug!(artifact_id = %artifact_id, "Retrieving artifact");

    let artifact_repo = ArtifactRepository::new(app_context.db_pool())
        .map_err(|e| ApiError::internal_error(format!("Database error: {e}")))?;

    let artifact_id_typed = ArtifactId::new(&artifact_id);
    match artifact_repo.get_artifact_by_id(&artifact_id_typed).await {
        Ok(Some(artifact)) => {
            tracing::debug!("Artifact retrieved successfully");
            Ok((StatusCode::OK, Json(artifact)).into_response())
        },
        Ok(None) => {
            tracing::debug!("Artifact not found");
            Err(ApiError::not_found(format!(
                "Artifact '{}' not found",
                artifact_id
            )))
        },
        Err(e) => {
            tracing::error!(error = %e, "Failed to retrieve artifact");
            Err(ApiError::internal_error("Failed to retrieve artifact"))
        },
    }
}

pub async fn list_artifacts_by_user(
    Extension(req_ctx): Extension<RequestContext>,
    State(app_context): State<AppContext>,
    Query(params): Query<ArtifactQueryParams>,
) -> Result<impl IntoResponse, ApiError> {
    let user_id = req_ctx.auth.user_id.as_str();

    tracing::debug!(user_id = %user_id, "Listing artifacts by user");

    let artifact_repo = ArtifactRepository::new(app_context.db_pool())
        .map_err(|e| ApiError::internal_error(format!("Database error: {e}")))?;

    let user_id_typed = UserId::new(user_id);
    let artifacts = artifact_repo
        .get_artifacts_by_user_id(&user_id_typed, params.limit.map(|l| l as i32))
        .await
        .map_err(|e| {
            tracing::error!(error = %e, "Failed to list artifacts");
            ApiError::internal_error("Failed to retrieve artifacts")
        })?;

    tracing::debug!(
        user_id = %user_id,
        count = artifacts.len(),
        "Artifacts listed"
    );
    Ok((StatusCode::OK, Json(artifacts)))
}

pub async fn get_artifact_ui(
    Extension(_req_ctx): Extension<RequestContext>,
    State(app_context): State<AppContext>,
    Path(artifact_id): Path<String>,
) -> Result<Response, ApiError> {
    tracing::debug!(artifact_id = %artifact_id, "Rendering artifact as MCP App UI");

    let artifact_repo = ArtifactRepository::new(app_context.db_pool())
        .map_err(|e| ApiError::internal_error(format!("Database error: {e}")))?;
    let artifact_id_typed = ArtifactId::new(&artifact_id);

    let artifact = artifact_repo
        .get_artifact_by_id(&artifact_id_typed)
        .await
        .map_err(|e| {
            tracing::error!(error = %e, "Failed to retrieve artifact");
            ApiError::internal_error("Failed to retrieve artifact")
        })?
        .ok_or_else(|| ApiError::not_found(format!("Artifact '{}' not found", artifact_id)))?;

    let registry = create_default_registry();
    let artifact_type = &artifact.metadata.artifact_type;

    if !registry.supports(artifact_type) {
        tracing::warn!(artifact_type = %artifact_type, "No UI renderer for artifact type");
        return Err(ApiError::bad_request(format!(
            "No UI renderer available for artifact type '{}'",
            artifact_type
        )));
    }

    let ui_resource: systemprompt_mcp::services::ui_renderer::UiResource =
        registry.render(&artifact).await.map_err(|e| {
            tracing::error!(error = %e, "Failed to render artifact UI");
            ApiError::internal_error("Failed to render artifact UI")
        })?;

    tracing::debug!(artifact_id = %artifact_id, "Artifact UI rendered successfully");

    Response::builder()
        .status(StatusCode::OK)
        .header(header::CONTENT_TYPE, MCP_APP_MIME_TYPE)
        .header(
            header::CONTENT_SECURITY_POLICY,
            ui_resource.csp.to_header_value(),
        )
        .header(header::X_FRAME_OPTIONS, "SAMEORIGIN")
        .body(axum::body::Body::from(ui_resource.html))
        .map_err(|e| ApiError::internal_error(format!("Failed to build response: {e}")))
}