systemprompt_api/routes/agent/
artifacts.rs1use 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}