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