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