cognee_http_server/routers/
visualize.rs1use axum::{
17 Router,
18 extract::{Query, State},
19 http::StatusCode,
20 response::Html,
21 routing::{get, post},
22};
23
24use cognee_database::IngestDb;
25
26use crate::auth::AuthenticatedUser;
27use crate::dto::visualize::{UserDatasetPairDTO, VisualizeQueryDTO};
28use crate::error::ApiError;
29use crate::middleware::validation::Json as ValidatedJson;
30use crate::state::AppState;
31
32pub fn router() -> Router<AppState> {
34 Router::new()
35 .route("/", get(get_visualize))
36 .route("/multi", post(post_visualize_multi))
37}
38
39#[utoipa::path(
47 get,
48 path = "/api/v1/visualize",
49 tag = "visualize",
50 params(("dataset_id" = uuid::Uuid, Query, description = "Target dataset")),
51 responses(
52 (status = 200, description = "HTML visualization", content_type = "text/html"),
53 (status = 401, description = "unauthorized"),
54 (status = 409, description = "catch-all"),
55 (status = 422, description = "missing or malformed dataset_id"),
56 )
57)]
58#[tracing::instrument(name = "cognee.api.visualize", skip(state), fields(cognee.dataset.id = %query.dataset_id))]
59pub async fn get_visualize(
60 user: AuthenticatedUser,
61 State(state): State<AppState>,
62 Query(query): Query<VisualizeQueryDTO>,
63) -> Result<Html<String>, ApiError> {
64 crate::telemetry::emit(
65 "Visualize API Endpoint Invoked",
66 user.id,
67 serde_json::json!({
68 "endpoint": "GET /v1/visualize",
69 "dataset_id": query.dataset_id.to_string(),
70 }),
71 );
72
73 let components = state.components().ok_or_else(|| {
74 ApiError::VisualizeError(StatusCode::CONFLICT, "components not wired".into())
75 })?;
76
77 let db = components.database.clone();
80 let dataset = IngestDb::get_dataset(db.as_ref(), query.dataset_id)
81 .await
82 .map_err(|err| ApiError::VisualizeError(StatusCode::CONFLICT, err.to_string()))?
83 .ok_or_else(|| {
84 ApiError::VisualizeError(
85 StatusCode::CONFLICT,
86 format!("dataset {} not found", query.dataset_id),
87 )
88 })?;
89 let allowed = if let Some(ref acl) = components.acl_db {
90 acl.has_permission(user.id, dataset.id, "read")
91 .await
92 .map_err(|err| ApiError::VisualizeError(StatusCode::CONFLICT, err.to_string()))?
93 } else {
94 true
96 };
97 if !allowed {
98 return Err(ApiError::VisualizeError(
99 StatusCode::CONFLICT,
100 "permission denied".to_string(),
101 ));
102 }
103
104 let Some(graph_db) = components.graph_db.clone() else {
105 return Err(ApiError::VisualizeError(
106 StatusCode::CONFLICT,
107 "graph database is not wired".to_string(),
108 ));
109 };
110
111 let html = cognee_visualization::render(graph_db.as_ref())
112 .await
113 .map_err(|err| ApiError::VisualizeError(StatusCode::CONFLICT, err.to_string()))?;
114 Ok(Html(html))
115}
116
117#[utoipa::path(
124 post,
125 path = "/api/v1/visualize/multi",
126 tag = "visualize",
127 request_body = Vec<UserDatasetPairDTO>,
128 responses(
129 (status = 200, description = "HTML visualization", content_type = "text/html"),
130 (status = 401, description = "unauthorized"),
131 (status = 403, description = "superuser required"),
132 (status = 409, description = "catch-all"),
133 )
134)]
135#[tracing::instrument(name = "cognee.api.visualize.multi", skip(state, pairs))]
136pub async fn post_visualize_multi(
137 user: AuthenticatedUser,
138 State(state): State<AppState>,
139 ValidatedJson(pairs): ValidatedJson<Vec<UserDatasetPairDTO>>,
140) -> Result<Html<String>, ApiError> {
141 if !user.is_superuser {
142 return Err(ApiError::VisualizeError(
145 StatusCode::FORBIDDEN,
146 "Superuser privileges required for multi-user visualization".to_string(),
147 ));
148 }
149 crate::telemetry::emit(
150 "Visualize Multi API Endpoint Invoked",
151 user.id,
152 serde_json::json!({
153 "endpoint": "POST /v1/visualize/multi",
154 "pair_count": pairs.len(),
155 }),
156 );
157
158 let components = state.components().ok_or_else(|| {
159 ApiError::VisualizeError(StatusCode::CONFLICT, "components not wired".into())
160 })?;
161
162 let db = components.database.clone();
165 let mut user_pairs: Vec<(String, std::sync::Arc<dyn cognee_graph::GraphDBTrait>)> = Vec::new();
166 for pair in &pairs {
167 let dataset = IngestDb::get_dataset(db.as_ref(), pair.dataset_id)
168 .await
169 .map_err(|err| ApiError::VisualizeError(StatusCode::CONFLICT, err.to_string()))?
170 .ok_or_else(|| {
171 ApiError::VisualizeError(
172 StatusCode::CONFLICT,
173 format!("dataset {} not found", pair.dataset_id),
174 )
175 })?;
176 let allowed = if let Some(ref acl) = components.acl_db {
180 acl.has_permission(pair.user_id, dataset.id, "read")
181 .await
182 .map_err(|err| ApiError::VisualizeError(StatusCode::CONFLICT, err.to_string()))?
183 } else {
184 true
185 };
186 if !allowed {
187 return Err(ApiError::VisualizeError(
188 StatusCode::CONFLICT,
189 "permission denied".to_string(),
190 ));
191 }
192 let Some(graph_db) = components.graph_db.clone() else {
193 return Err(ApiError::VisualizeError(
194 StatusCode::CONFLICT,
195 "graph database is not wired".to_string(),
196 ));
197 };
198
199 let user_label = pair.user_id.to_string();
204
205 user_pairs.push((user_label, graph_db));
206 }
207
208 let html = cognee_visualization::render_multi_user(&user_pairs)
209 .await
210 .map_err(|err| ApiError::VisualizeError(StatusCode::CONFLICT, err.to_string()))?;
211 Ok(Html(html))
212}