1use crate::routes::errors::ErrorResponse;
2use crate::routes::recipe_utils::{apply_recipe_to_agent, build_recipe_with_parameter_values};
3use crate::state::AppState;
4use aster::recipe::Recipe;
5use aster::session::session_manager::SessionInsights;
6use aster::session::{Session, SessionManager};
7use axum::extract::State;
8use axum::routing::post;
9use axum::{
10 extract::Path,
11 http::StatusCode,
12 routing::{delete, get, put},
13 Json, Router,
14};
15use serde::{Deserialize, Serialize};
16use std::collections::HashMap;
17use std::sync::Arc;
18use utoipa::ToSchema;
19
20#[derive(Serialize, ToSchema)]
21#[serde(rename_all = "camelCase")]
22pub struct SessionListResponse {
23 sessions: Vec<Session>,
25}
26
27#[derive(Deserialize, ToSchema)]
28#[serde(rename_all = "camelCase")]
29pub struct UpdateSessionNameRequest {
30 name: String,
32}
33
34#[derive(Deserialize, ToSchema)]
35#[serde(rename_all = "camelCase")]
36pub struct UpdateSessionUserRecipeValuesRequest {
37 user_recipe_values: HashMap<String, String>,
39}
40
41#[derive(Debug, Serialize, ToSchema)]
42pub struct UpdateSessionUserRecipeValuesResponse {
43 recipe: Recipe,
44}
45
46#[derive(Deserialize, ToSchema)]
47#[serde(rename_all = "camelCase")]
48pub struct ImportSessionRequest {
49 json: String,
50}
51
52#[derive(Debug, Deserialize, ToSchema)]
53#[serde(rename_all = "lowercase")]
54pub enum EditType {
55 Fork,
56 Edit,
57}
58
59#[derive(Deserialize, ToSchema)]
60#[serde(rename_all = "camelCase")]
61pub struct EditMessageRequest {
62 timestamp: i64,
63 #[serde(default = "default_edit_type")]
64 edit_type: EditType,
65}
66
67fn default_edit_type() -> EditType {
68 EditType::Fork
69}
70
71#[derive(Serialize, ToSchema)]
72#[serde(rename_all = "camelCase")]
73pub struct EditMessageResponse {
74 session_id: String,
75}
76
77const MAX_NAME_LENGTH: usize = 200;
78
79#[utoipa::path(
80 get,
81 path = "/sessions",
82 responses(
83 (status = 200, description = "List of available sessions retrieved successfully", body = SessionListResponse),
84 (status = 401, description = "Unauthorized - Invalid or missing API key"),
85 (status = 500, description = "Internal server error")
86 ),
87 security(
88 ("api_key" = [])
89 ),
90 tag = "Session Management"
91)]
92async fn list_sessions() -> Result<Json<SessionListResponse>, StatusCode> {
93 let sessions = SessionManager::list_sessions()
94 .await
95 .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
96
97 Ok(Json(SessionListResponse { sessions }))
98}
99
100#[utoipa::path(
101 get,
102 path = "/sessions/{session_id}",
103 params(
104 ("session_id" = String, Path, description = "Unique identifier for the session")
105 ),
106 responses(
107 (status = 200, description = "Session history retrieved successfully", body = Session),
108 (status = 401, description = "Unauthorized - Invalid or missing API key"),
109 (status = 404, description = "Session not found"),
110 (status = 500, description = "Internal server error")
111 ),
112 security(
113 ("api_key" = [])
114 ),
115 tag = "Session Management"
116)]
117async fn get_session(Path(session_id): Path<String>) -> Result<Json<Session>, StatusCode> {
118 let session = SessionManager::get_session(&session_id, true)
119 .await
120 .map_err(|_| StatusCode::NOT_FOUND)?;
121
122 Ok(Json(session))
123}
124#[utoipa::path(
125 get,
126 path = "/sessions/insights",
127 responses(
128 (status = 200, description = "Session insights retrieved successfully", body = SessionInsights),
129 (status = 401, description = "Unauthorized - Invalid or missing API key"),
130 (status = 500, description = "Internal server error")
131 ),
132 security(
133 ("api_key" = [])
134 ),
135 tag = "Session Management"
136)]
137async fn get_session_insights() -> Result<Json<SessionInsights>, StatusCode> {
138 let insights = SessionManager::get_insights()
139 .await
140 .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
141 Ok(Json(insights))
142}
143
144#[utoipa::path(
145 put,
146 path = "/sessions/{session_id}/name",
147 request_body = UpdateSessionNameRequest,
148 params(
149 ("session_id" = String, Path, description = "Unique identifier for the session")
150 ),
151 responses(
152 (status = 200, description = "Session name updated successfully"),
153 (status = 400, description = "Bad request - Name too long (max 200 characters)"),
154 (status = 401, description = "Unauthorized - Invalid or missing API key"),
155 (status = 404, description = "Session not found"),
156 (status = 500, description = "Internal server error")
157 ),
158 security(
159 ("api_key" = [])
160 ),
161 tag = "Session Management"
162)]
163async fn update_session_name(
164 Path(session_id): Path<String>,
165 Json(request): Json<UpdateSessionNameRequest>,
166) -> Result<StatusCode, StatusCode> {
167 let name = request.name.trim();
168 if name.is_empty() {
169 return Err(StatusCode::BAD_REQUEST);
170 }
171 if name.len() > MAX_NAME_LENGTH {
172 return Err(StatusCode::BAD_REQUEST);
173 }
174
175 SessionManager::update_session(&session_id)
176 .user_provided_name(name.to_string())
177 .apply()
178 .await
179 .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
180
181 Ok(StatusCode::OK)
182}
183
184#[utoipa::path(
185 put,
186 path = "/sessions/{session_id}/user_recipe_values",
187 request_body = UpdateSessionUserRecipeValuesRequest,
188 params(
189 ("session_id" = String, Path, description = "Unique identifier for the session")
190 ),
191 responses(
192 (status = 200, description = "Session user recipe values updated successfully", body = UpdateSessionUserRecipeValuesResponse),
193 (status = 401, description = "Unauthorized - Invalid or missing API key"),
194 (status = 404, description = "Session not found", body = ErrorResponse),
195 (status = 500, description = "Internal server error", body = ErrorResponse)
196 ),
197 security(
198 ("api_key" = [])
199 ),
200 tag = "Session Management"
201)]
202async fn update_session_user_recipe_values(
204 State(state): State<Arc<AppState>>,
205 Path(session_id): Path<String>,
206 Json(request): Json<UpdateSessionUserRecipeValuesRequest>,
207) -> Result<Json<UpdateSessionUserRecipeValuesResponse>, ErrorResponse> {
208 SessionManager::update_session(&session_id)
209 .user_recipe_values(Some(request.user_recipe_values))
210 .apply()
211 .await
212 .map_err(|err| ErrorResponse {
213 message: err.to_string(),
214 status: StatusCode::INTERNAL_SERVER_ERROR,
215 })?;
216
217 let session = SessionManager::get_session(&session_id, false)
218 .await
219 .map_err(|err| ErrorResponse {
220 message: err.to_string(),
221 status: StatusCode::INTERNAL_SERVER_ERROR,
222 })?;
223 let recipe = session.recipe.ok_or_else(|| ErrorResponse {
224 message: "Recipe not found".to_string(),
225 status: StatusCode::NOT_FOUND,
226 })?;
227
228 let user_recipe_values = session.user_recipe_values.unwrap_or_default();
229 match build_recipe_with_parameter_values(&recipe, user_recipe_values).await {
230 Ok(Some(recipe)) => {
231 let agent = state
232 .get_agent_for_route(session_id.clone())
233 .await
234 .map_err(|status| ErrorResponse {
235 message: format!("Failed to get agent: {}", status),
236 status,
237 })?;
238 if let Some(prompt) = apply_recipe_to_agent(&agent, &recipe, false).await {
239 agent.extend_system_prompt(prompt).await;
240 }
241 Ok(Json(UpdateSessionUserRecipeValuesResponse { recipe }))
242 }
243 Ok(None) => Err(ErrorResponse {
244 message: "Missing required parameters".to_string(),
245 status: StatusCode::BAD_REQUEST,
246 }),
247 Err(e) => Err(ErrorResponse {
248 message: e.to_string(),
249 status: StatusCode::INTERNAL_SERVER_ERROR,
250 }),
251 }
252}
253
254#[utoipa::path(
255 delete,
256 path = "/sessions/{session_id}",
257 params(
258 ("session_id" = String, Path, description = "Unique identifier for the session")
259 ),
260 responses(
261 (status = 200, description = "Session deleted successfully"),
262 (status = 401, description = "Unauthorized - Invalid or missing API key"),
263 (status = 404, description = "Session not found"),
264 (status = 500, description = "Internal server error")
265 ),
266 security(
267 ("api_key" = [])
268 ),
269 tag = "Session Management"
270)]
271async fn delete_session(Path(session_id): Path<String>) -> Result<StatusCode, StatusCode> {
272 SessionManager::delete_session(&session_id)
273 .await
274 .map_err(|e| {
275 if e.to_string().contains("not found") {
276 StatusCode::NOT_FOUND
277 } else {
278 StatusCode::INTERNAL_SERVER_ERROR
279 }
280 })?;
281
282 Ok(StatusCode::OK)
283}
284
285#[utoipa::path(
286 get,
287 path = "/sessions/{session_id}/export",
288 params(
289 ("session_id" = String, Path, description = "Unique identifier for the session")
290 ),
291 responses(
292 (status = 200, description = "Session exported successfully", body = String),
293 (status = 401, description = "Unauthorized - Invalid or missing API key"),
294 (status = 404, description = "Session not found"),
295 (status = 500, description = "Internal server error")
296 ),
297 security(
298 ("api_key" = [])
299 ),
300 tag = "Session Management"
301)]
302async fn export_session(Path(session_id): Path<String>) -> Result<Json<String>, StatusCode> {
303 let exported = SessionManager::export_session(&session_id)
304 .await
305 .map_err(|_| StatusCode::NOT_FOUND)?;
306
307 Ok(Json(exported))
308}
309
310#[utoipa::path(
311 post,
312 path = "/sessions/import",
313 request_body = ImportSessionRequest,
314 responses(
315 (status = 200, description = "Session imported successfully", body = Session),
316 (status = 401, description = "Unauthorized - Invalid or missing API key"),
317 (status = 400, description = "Bad request - Invalid JSON"),
318 (status = 500, description = "Internal server error")
319 ),
320 security(
321 ("api_key" = [])
322 ),
323 tag = "Session Management"
324)]
325async fn import_session(
326 Json(request): Json<ImportSessionRequest>,
327) -> Result<Json<Session>, StatusCode> {
328 let session = SessionManager::import_session(&request.json)
329 .await
330 .map_err(|_| StatusCode::BAD_REQUEST)?;
331
332 Ok(Json(session))
333}
334
335#[utoipa::path(
336 post,
337 path = "/sessions/{session_id}/edit_message",
338 request_body = EditMessageRequest,
339 params(
340 ("session_id" = String, Path, description = "Unique identifier for the session")
341 ),
342 responses(
343 (status = 200, description = "Session prepared for editing - frontend should submit the edited message", body = EditMessageResponse),
344 (status = 400, description = "Bad request - Invalid message timestamp"),
345 (status = 401, description = "Unauthorized - Invalid or missing API key"),
346 (status = 404, description = "Session or message not found"),
347 (status = 500, description = "Internal server error")
348 ),
349 security(
350 ("api_key" = [])
351 ),
352 tag = "Session Management"
353)]
354async fn edit_message(
355 Path(session_id): Path<String>,
356 Json(request): Json<EditMessageRequest>,
357) -> Result<Json<EditMessageResponse>, StatusCode> {
358 match request.edit_type {
359 EditType::Fork => {
360 let new_session = SessionManager::copy_session(&session_id, "(edited)".to_string())
361 .await
362 .map_err(|e| {
363 tracing::error!("Failed to copy session: {}", e);
364 aster::posthog::emit_error("session_copy_failed", &e.to_string());
365 StatusCode::INTERNAL_SERVER_ERROR
366 })?;
367
368 SessionManager::truncate_conversation(&new_session.id, request.timestamp)
369 .await
370 .map_err(|e| {
371 tracing::error!("Failed to truncate conversation: {}", e);
372 aster::posthog::emit_error("session_truncate_failed", &e.to_string());
373 StatusCode::INTERNAL_SERVER_ERROR
374 })?;
375
376 Ok(Json(EditMessageResponse {
377 session_id: new_session.id,
378 }))
379 }
380 EditType::Edit => {
381 SessionManager::truncate_conversation(&session_id, request.timestamp)
382 .await
383 .map_err(|e| {
384 tracing::error!("Failed to truncate conversation: {}", e);
385 aster::posthog::emit_error("session_truncate_failed", &e.to_string());
386 StatusCode::INTERNAL_SERVER_ERROR
387 })?;
388
389 Ok(Json(EditMessageResponse {
390 session_id: session_id.clone(),
391 }))
392 }
393 }
394}
395
396pub fn routes(state: Arc<AppState>) -> Router {
397 Router::new()
398 .route("/sessions", get(list_sessions))
399 .route("/sessions/{session_id}", get(get_session))
400 .route("/sessions/{session_id}", delete(delete_session))
401 .route("/sessions/{session_id}/export", get(export_session))
402 .route("/sessions/import", post(import_session))
403 .route("/sessions/insights", get(get_session_insights))
404 .route("/sessions/{session_id}/name", put(update_session_name))
405 .route(
406 "/sessions/{session_id}/user_recipe_values",
407 put(update_session_user_recipe_values),
408 )
409 .route("/sessions/{session_id}/edit_message", post(edit_message))
410 .with_state(state)
411}