Skip to main content

systemprompt_api/routes/agent/
artifacts.rs

1use axum::extract::{Path, Query, State};
2use axum::http::{header, StatusCode};
3use axum::response::{IntoResponse, Response};
4use axum::{Extension, Json};
5use serde::Deserialize;
6use systemprompt_models::api::ApiError;
7
8use systemprompt_agent::repository::content::ArtifactRepository;
9use systemprompt_identifiers::{ArtifactId, ContextId, TaskId, UserId};
10use systemprompt_mcp::services::ui_renderer::registry::create_default_registry;
11use systemprompt_mcp::services::ui_renderer::MCP_APP_MIME_TYPE;
12use systemprompt_models::RequestContext;
13use systemprompt_runtime::AppContext;
14
15#[derive(Debug, Clone, Copy, Deserialize)]
16pub struct ArtifactQueryParams {
17    limit: Option<u32>,
18}
19
20pub async fn list_artifacts_by_context(
21    Extension(_req_ctx): Extension<RequestContext>,
22    State(app_context): State<AppContext>,
23    Path(context_id): Path<String>,
24) -> Result<impl IntoResponse, ApiError> {
25    tracing::debug!(context_id = %context_id, "Listing artifacts by context");
26
27    let artifact_repo = ArtifactRepository::new(app_context.db_pool().clone());
28
29    let context_id_typed = ContextId::new(&context_id);
30    let artifacts = artifact_repo
31        .get_artifacts_by_context(&context_id_typed)
32        .await
33        .map_err(|e| {
34            tracing::error!(error = %e, "Failed to list artifacts");
35            ApiError::internal_error("Failed to retrieve artifacts")
36        })?;
37
38    tracing::debug!(
39        context_id = %context_id,
40        count = artifacts.len(),
41        "Artifacts listed"
42    );
43    Ok((StatusCode::OK, Json(artifacts)))
44}
45
46pub async fn list_artifacts_by_task(
47    Extension(_req_ctx): Extension<RequestContext>,
48    State(app_context): State<AppContext>,
49    Path(task_id): Path<String>,
50) -> Result<impl IntoResponse, ApiError> {
51    tracing::debug!(task_id = %task_id, "Listing artifacts by task");
52
53    let artifact_repo = ArtifactRepository::new(app_context.db_pool().clone());
54
55    let task_id_typed = TaskId::new(&task_id);
56    let artifacts = artifact_repo
57        .get_artifacts_by_task(&task_id_typed)
58        .await
59        .map_err(|e| {
60            tracing::error!(error = %e, "Failed to list artifacts");
61            ApiError::internal_error("Failed to retrieve artifacts")
62        })?;
63
64    tracing::debug!(
65        task_id = %task_id,
66        count = artifacts.len(),
67        "Artifacts listed"
68    );
69    Ok((StatusCode::OK, Json(artifacts)))
70}
71
72pub async fn get_artifact(
73    Extension(_req_ctx): Extension<RequestContext>,
74    State(app_context): State<AppContext>,
75    Path(artifact_id): Path<String>,
76) -> Result<impl IntoResponse, ApiError> {
77    tracing::debug!(artifact_id = %artifact_id, "Retrieving artifact");
78
79    let artifact_repo = ArtifactRepository::new(app_context.db_pool().clone());
80
81    let artifact_id_typed = ArtifactId::new(&artifact_id);
82    match artifact_repo.get_artifact_by_id(&artifact_id_typed).await {
83        Ok(Some(artifact)) => {
84            tracing::debug!("Artifact retrieved successfully");
85            Ok((StatusCode::OK, Json(artifact)).into_response())
86        },
87        Ok(None) => {
88            tracing::debug!("Artifact not found");
89            Err(ApiError::not_found(format!(
90                "Artifact '{}' not found",
91                artifact_id
92            )))
93        },
94        Err(e) => {
95            tracing::error!(error = %e, "Failed to retrieve artifact");
96            Err(ApiError::internal_error("Failed to retrieve artifact"))
97        },
98    }
99}
100
101pub async fn list_artifacts_by_user(
102    Extension(req_ctx): Extension<RequestContext>,
103    State(app_context): State<AppContext>,
104    Query(params): Query<ArtifactQueryParams>,
105) -> Result<impl IntoResponse, ApiError> {
106    let user_id = req_ctx.auth.user_id.as_str();
107
108    tracing::debug!(user_id = %user_id, "Listing artifacts by user");
109
110    let artifact_repo = ArtifactRepository::new(app_context.db_pool().clone());
111
112    let user_id_typed = UserId::new(user_id);
113    let artifacts = artifact_repo
114        .get_artifacts_by_user_id(&user_id_typed, params.limit.map(|l| l as i32))
115        .await
116        .map_err(|e| {
117            tracing::error!(error = %e, "Failed to list artifacts");
118            ApiError::internal_error("Failed to retrieve artifacts")
119        })?;
120
121    tracing::debug!(
122        user_id = %user_id,
123        count = artifacts.len(),
124        "Artifacts listed"
125    );
126    Ok((StatusCode::OK, Json(artifacts)))
127}
128
129pub async fn get_artifact_ui(
130    Extension(_req_ctx): Extension<RequestContext>,
131    State(app_context): State<AppContext>,
132    Path(artifact_id): Path<String>,
133) -> Result<Response, ApiError> {
134    tracing::debug!(artifact_id = %artifact_id, "Rendering artifact as MCP App UI");
135
136    let artifact_repo = ArtifactRepository::new(app_context.db_pool().clone());
137    let artifact_id_typed = ArtifactId::new(&artifact_id);
138
139    let artifact = artifact_repo
140        .get_artifact_by_id(&artifact_id_typed)
141        .await
142        .map_err(|e| {
143            tracing::error!(error = %e, "Failed to retrieve artifact");
144            ApiError::internal_error("Failed to retrieve artifact")
145        })?
146        .ok_or_else(|| ApiError::not_found(format!("Artifact '{}' not found", artifact_id)))?;
147
148    let registry = create_default_registry();
149    let artifact_type = &artifact.metadata.artifact_type;
150
151    if !registry.supports(artifact_type) {
152        tracing::warn!(artifact_type = %artifact_type, "No UI renderer for artifact type");
153        return Err(ApiError::bad_request(format!(
154            "No UI renderer available for artifact type '{}'",
155            artifact_type
156        )));
157    }
158
159    let ui_resource: systemprompt_mcp::services::ui_renderer::UiResource =
160        registry.render(&artifact).await.map_err(|e| {
161            tracing::error!(error = %e, "Failed to render artifact UI");
162            ApiError::internal_error("Failed to render artifact UI")
163        })?;
164
165    tracing::debug!(artifact_id = %artifact_id, "Artifact UI rendered successfully");
166
167    Ok(Response::builder()
168        .status(StatusCode::OK)
169        .header(header::CONTENT_TYPE, MCP_APP_MIME_TYPE)
170        .header(
171            header::CONTENT_SECURITY_POLICY,
172            ui_resource.csp.to_header_value(),
173        )
174        .header(header::X_FRAME_OPTIONS, "SAMEORIGIN")
175        .body(axum::body::Body::from(ui_resource.html))
176        .expect("Response builder should not fail with valid headers"))
177}