systemprompt_api/routes/agent/
artifacts.rs1use axum::extract::{Path, Query, State};
11use axum::http::{StatusCode, header};
12use axum::response::{IntoResponse, Response};
13use axum::{Extension, Json};
14use serde::Deserialize;
15
16use systemprompt_identifiers::{ArtifactId, TaskId};
17use systemprompt_mcp::McpDomainError;
18use systemprompt_mcp::services::ui_renderer::MCP_APP_MIME_TYPE;
19use systemprompt_mcp::services::ui_renderer::registry::{
20 create_default_registry, resolve_artifact_type,
21};
22use systemprompt_models::RequestContext;
23use systemprompt_models::api::ApiError;
24use systemprompt_runtime::AppContext;
25
26use crate::error::ApiHttpError;
27
28#[derive(Debug, Clone, Copy, Deserialize)]
29pub struct ArtifactQueryParams {
30 pub limit: Option<u32>,
31}
32
33pub async fn list_artifacts_by_context(
34 Extension(req_ctx): Extension<RequestContext>,
35 State(app_context): State<AppContext>,
36 Path(context_id): Path<String>,
37) -> Result<impl IntoResponse, ApiHttpError> {
38 tracing::debug!(context_id = %context_id, "Listing artifacts by context");
39
40 let context_id_typed = super::parse_context_id(&context_id)?;
41
42 let context_repo = app_context.a2a_repositories().contexts.clone();
43 context_repo
44 .validate_context_ownership(&context_id_typed, req_ctx.user_id())
45 .await?;
46
47 let artifact_repo = app_context.a2a_repositories().artifacts.clone();
48 let artifacts = artifact_repo
49 .get_artifacts_by_context(&context_id_typed)
50 .await?;
51
52 tracing::debug!(
53 context_id = %context_id,
54 count = artifacts.len(),
55 "Artifacts listed"
56 );
57 Ok((StatusCode::OK, Json(artifacts)))
58}
59
60pub async fn list_artifacts_by_task(
61 Extension(req_ctx): Extension<RequestContext>,
62 State(app_context): State<AppContext>,
63 Path(task_id): Path<String>,
64) -> Result<impl IntoResponse, ApiHttpError> {
65 tracing::debug!(task_id = %task_id, "Listing artifacts by task");
66
67 let task_id_typed = TaskId::try_new(&task_id).map_err(ApiError::from)?;
68
69 let task_repo = app_context.a2a_repositories().tasks.clone();
70 task_repo
71 .validate_task_ownership(&task_id_typed, req_ctx.user_id())
72 .await?;
73
74 let artifact_repo = app_context.a2a_repositories().artifacts.clone();
75 let artifacts = artifact_repo.get_artifacts_by_task(&task_id_typed).await?;
76
77 tracing::debug!(
78 task_id = %task_id,
79 count = artifacts.len(),
80 "Artifacts listed"
81 );
82 Ok((StatusCode::OK, Json(artifacts)))
83}
84
85pub async fn get_artifact(
86 Extension(req_ctx): Extension<RequestContext>,
87 State(app_context): State<AppContext>,
88 Path(artifact_id): Path<String>,
89) -> Result<impl IntoResponse, ApiHttpError> {
90 tracing::debug!(artifact_id = %artifact_id, "Retrieving artifact");
91
92 let artifact_repo = app_context.a2a_repositories().artifacts.clone();
93
94 let artifact_id_typed = ArtifactId::try_new(&artifact_id).map_err(ApiError::from)?;
95 artifact_repo
96 .validate_artifact_ownership(&artifact_id_typed, req_ctx.user_id())
97 .await?;
98
99 let artifact = artifact_repo
100 .find_artifact_by_id(&artifact_id_typed)
101 .await?
102 .ok_or_else(|| ApiHttpError::not_found(format!("Artifact '{artifact_id}' not found")))?;
103
104 tracing::debug!("Artifact retrieved successfully");
105 Ok((StatusCode::OK, Json(artifact)))
106}
107
108pub async fn list_artifacts_by_user(
109 Extension(req_ctx): Extension<RequestContext>,
110 State(app_context): State<AppContext>,
111 Query(params): Query<ArtifactQueryParams>,
112) -> Result<impl IntoResponse, ApiHttpError> {
113 let user_id = req_ctx.user_id();
114
115 tracing::debug!(user_id = %user_id, "Listing artifacts by user");
116
117 let artifact_repo = app_context.a2a_repositories().artifacts.clone();
118
119 let limit = params.limit.map(|l| i32::try_from(l).unwrap_or(i32::MAX));
120 let artifacts = artifact_repo
121 .get_artifacts_by_user_id(user_id, limit)
122 .await?;
123
124 tracing::debug!(
125 user_id = %user_id,
126 count = artifacts.len(),
127 "Artifacts listed"
128 );
129 Ok((StatusCode::OK, Json(artifacts)))
130}
131
132pub async fn get_artifact_ui(
133 Extension(req_ctx): Extension<RequestContext>,
134 State(app_context): State<AppContext>,
135 Path(artifact_id): Path<String>,
136) -> Result<Response, ApiHttpError> {
137 tracing::debug!(artifact_id = %artifact_id, "Rendering artifact as MCP App UI");
138
139 let artifact_repo = app_context.a2a_repositories().artifacts.clone();
140 let artifact_id_typed = ArtifactId::try_new(&artifact_id).map_err(ApiError::from)?;
141
142 artifact_repo
143 .validate_artifact_ownership(&artifact_id_typed, req_ctx.user_id())
144 .await?;
145
146 let artifact = artifact_repo
147 .find_artifact_by_id(&artifact_id_typed)
148 .await?
149 .ok_or_else(|| ApiHttpError::not_found(format!("Artifact '{artifact_id}' not found")))?;
150
151 let registry = create_default_registry();
152 let artifact_type = resolve_artifact_type(&artifact);
153
154 if !registry.supports(artifact_type) {
155 tracing::warn!(artifact_type = %artifact_type, "No UI renderer for artifact type");
156 return Err(ApiHttpError::bad_request(format!(
157 "No UI renderer available for artifact type '{artifact_type}'"
158 )));
159 }
160
161 let ui_resource: systemprompt_mcp::services::ui_renderer::UiResource = registry
162 .render(&artifact)
163 .map_err(ArtifactUiError::Render)?;
164
165 tracing::debug!(artifact_id = %artifact_id, "Artifact UI rendered successfully");
166
167 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 .map_err(|e| ArtifactUiError::Response(e).into())
177}
178
179#[derive(Debug, thiserror::Error)]
180enum ArtifactUiError {
181 #[error("failed to render artifact UI")]
182 Render(#[source] McpDomainError),
183 #[error("failed to build artifact UI response")]
184 Response(#[source] http::Error),
185}
186
187impl From<ArtifactUiError> for ApiHttpError {
188 fn from(err: ArtifactUiError) -> Self {
189 let context = match &err {
190 ArtifactUiError::Render(_) => "Failed to render artifact UI",
191 ArtifactUiError::Response(_) => "Failed to build response",
192 };
193 ApiError::internal(context, err).into()
194 }
195}