Skip to main content

aster_server/routes/
session.rs

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    /// List of available session information objects
24    sessions: Vec<Session>,
25}
26
27#[derive(Deserialize, ToSchema)]
28#[serde(rename_all = "camelCase")]
29pub struct UpdateSessionNameRequest {
30    /// Updated name for the session (max 200 characters)
31    name: String,
32}
33
34#[derive(Deserialize, ToSchema)]
35#[serde(rename_all = "camelCase")]
36pub struct UpdateSessionUserRecipeValuesRequest {
37    /// Recipe parameter values entered by the user
38    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)]
202// Update session user recipe parameter values
203async 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}