use aonyx_core::{Message, Role};
use aonyx_memory::SessionRecord;
use axum::extract::{Path, Query, State};
use axum::http::StatusCode;
use axum::Json;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::agent::last_assistant_text;
use crate::error::{ApiError, ApiResult};
use crate::state::ApiState;
#[derive(Debug, Deserialize)]
pub struct ListParams {
pub project: Option<String>,
pub limit: Option<usize>,
}
#[derive(Debug, Serialize)]
pub struct SessionSummary {
pub id: Uuid,
pub project: String,
pub title: String,
pub turns: u32,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub parent_id: Option<Uuid>,
}
impl From<SessionRecord> for SessionSummary {
fn from(r: SessionRecord) -> Self {
Self {
id: r.id,
project: r.project,
title: r.title,
turns: r.turns,
created_at: r.created_at,
updated_at: r.updated_at,
parent_id: r.parent_id,
}
}
}
#[derive(Debug, Deserialize)]
pub struct NewSessionRequest {
pub project: Option<String>,
pub system_prompt: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct SendMessageRequest {
pub content: String,
}
#[derive(Debug, Serialize)]
pub struct TurnResponse {
pub reply: String,
pub session: SessionRecord,
}
pub async fn list_sessions(
State(state): State<ApiState>,
Query(params): Query<ListParams>,
) -> ApiResult<Json<Vec<SessionSummary>>> {
let project = state.project_or_default(params.project);
let limit = params.limit.unwrap_or(50).clamp(1, 500);
let rows = state.sessions.list_by_project(&project, limit).await?;
Ok(Json(rows.into_iter().map(SessionSummary::from).collect()))
}
pub async fn create_session(
State(state): State<ApiState>,
Json(req): Json<NewSessionRequest>,
) -> ApiResult<(StatusCode, Json<SessionRecord>)> {
let project = state.project_or_default(req.project);
let mut seed = Vec::new();
if let Some(prompt) = req.system_prompt.filter(|p| !p.trim().is_empty()) {
seed.push(Message::new(Role::System, prompt));
}
let record = state.sessions.create(&project, seed).await?;
Ok((StatusCode::CREATED, Json(record)))
}
pub async fn get_session(
State(state): State<ApiState>,
Path(id): Path<Uuid>,
) -> ApiResult<Json<SessionRecord>> {
let record = state
.sessions
.get(id)
.await?
.ok_or_else(|| ApiError::NotFound(format!("no session {id}")))?;
Ok(Json(record))
}
pub async fn delete_session(
State(state): State<ApiState>,
Path(id): Path<Uuid>,
) -> ApiResult<StatusCode> {
state.sessions.delete(id).await?;
Ok(StatusCode::NO_CONTENT)
}
pub async fn send_message(
State(state): State<ApiState>,
Path(id): Path<Uuid>,
Json(req): Json<SendMessageRequest>,
) -> ApiResult<Json<TurnResponse>> {
if req.content.trim().is_empty() {
return Err(ApiError::BadRequest("empty message content".into()));
}
let record = state
.sessions
.get(id)
.await?
.ok_or_else(|| ApiError::NotFound(format!("no session {id}")))?;
let mut history = record.messages;
history.push(Message::new(Role::User, req.content));
let new_log = state.agent.run_turn(history).await?;
let reply = last_assistant_text(&new_log);
state.sessions.update(id, new_log, record.turns + 1).await?;
let session = state
.sessions
.get(id)
.await?
.ok_or_else(|| ApiError::Internal("session vanished after update".into()))?;
Ok(Json(TurnResponse { reply, session }))
}