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