This commit is contained in:
2025-08-01 21:44:37 +02:00
parent 3faec1262b
commit 16f7ea5a8d
5 changed files with 217 additions and 1 deletions

2
Cargo.lock generated
View File

@@ -3535,8 +3535,10 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"chrono",
"config 0.13.4",
"dashmap 6.1.0",
"dirs 5.0.1",
"futures",
"futures-util",
"goose",

View File

@@ -23,6 +23,9 @@ futures-util = "0.3"
# For session IDs
uuid = { version = "1", features = ["serde", "v4"] }
dashmap = "6"
# For session listing
dirs = "5"
chrono = "0.4"
# Add dynamic-library for extension loading
[dev-dependencies]

View File

@@ -53,6 +53,39 @@ pub struct EndSessionRequest {
pub session_id: Uuid,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GetSessionRequest {
pub session_id: Uuid,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SessionInfo {
pub id: String,
pub name: String,
pub modified: String,
pub message_count: usize,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ListSessionsResponse {
pub sessions: Vec<SessionInfo>,
pub status: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SessionMessage {
pub role: String,
pub content: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GetSessionResponse {
pub session_id: String,
pub name: String,
pub messages: Vec<SessionMessage>,
pub status: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SummarizeSessionRequest {
pub session_id: Uuid,
@@ -585,6 +618,158 @@ pub async fn metrics_handler() -> Result<impl warp::Reply, Rejection> {
Ok(warp::reply::json(&resp))
}
pub async fn list_sessions_handler(
_api_key: String,
) -> Result<impl warp::Reply, Rejection> {
info!("Listing all sessions");
// Get all session files from the goose session directory
let data_dir = dirs::data_local_dir()
.ok_or_else(|| custom(AnyhowRejection(anyhow::anyhow!("Failed to get data directory"))))?;
let sessions_dir = data_dir.join("goose").join("sessions");
let mut sessions = Vec::new();
if sessions_dir.exists() {
match std::fs::read_dir(&sessions_dir) {
Ok(entries) => {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|ext| ext.to_str()) == Some("jsonl") {
// Read the session file to get metadata
match session::read_metadata(&path) {
Ok(metadata) => {
if let Ok(file_metadata) = entry.metadata() {
if let Ok(modified) = file_metadata.modified() {
let modified_str = chrono::DateTime::<chrono::Utc>::from(modified)
.format("%Y-%m-%d %H:%M:%S UTC")
.to_string();
let session_id = path.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown")
.to_string();
sessions.push(SessionInfo {
id: session_id,
name: metadata.description,
modified: modified_str,
message_count: metadata.message_count,
});
}
}
},
Err(e) => {
warn!("Failed to read metadata for {:?}: {}", path, e);
}
}
}
}
}
Err(e) => {
warn!("Failed to read sessions directory: {}", e);
}
}
}
// Sort by modified date (newest first)
sessions.sort_by(|a, b| b.modified.cmp(&a.modified));
let response = ListSessionsResponse {
sessions,
status: "success".to_string(),
};
Ok(warp::reply::with_status(
warp::reply::json(&response),
warp::http::StatusCode::OK,
))
}
pub async fn get_session_handler(
req: GetSessionRequest,
_api_key: String,
) -> Result<impl warp::Reply, Rejection> {
info!("Getting session: {}", req.session_id);
let session_name = req.session_id.to_string();
let session_path = session::get_path(Identifier::Name(session_name.clone()))
.map_err(|e| custom(AnyhowRejection(anyhow::anyhow!("Failed to get session path: {}", e))))?;
// Check if session file exists
if !session_path.exists() {
let response = GetSessionResponse {
session_id: req.session_id.to_string(),
name: String::new(),
messages: Vec::new(),
status: "error".to_string(),
};
return Ok(warp::reply::with_status(
warp::reply::json(&response),
warp::http::StatusCode::NOT_FOUND,
));
}
// Read session metadata and messages
let metadata = session::read_metadata(&session_path)
.map_err(|e| custom(AnyhowRejection(anyhow::anyhow!("Failed to read session metadata: {}", e))))?;
let messages = session::read_messages(&session_path)
.map_err(|e| custom(AnyhowRejection(anyhow::anyhow!("Failed to read session messages: {}", e))))?;
// Convert messages to API format
let api_messages: Vec<SessionMessage> = messages.iter()
.enumerate()
.map(|(idx, msg)| {
// Messages typically alternate between user and assistant
// First message is usually user, then assistant, and so on
// We can also serialize the message and check the role field
let role = if let Ok(serialized) = serde_json::to_value(msg) {
if let Some(role_value) = serialized.get("role") {
if let Some(role_str) = role_value.as_str() {
role_str.to_lowercase()
} else {
// Fallback to alternating pattern
if idx % 2 == 0 { "user" } else { "assistant" }.to_string()
}
} else {
// Fallback to alternating pattern
if idx % 2 == 0 { "user" } else { "assistant" }.to_string()
}
} else {
// Fallback to alternating pattern
if idx % 2 == 0 { "user" } else { "assistant" }.to_string()
};
// Extract text content, filtering out thinking tags
let content = msg.content
.iter()
.filter_map(|c| match c {
MessageContent::Text(text) => Some(text.text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n")
.trim()
.to_string();
SessionMessage { role, content }
})
.collect();
let response = GetSessionResponse {
session_id: req.session_id.to_string(),
name: metadata.description,
messages: api_messages,
status: "success".to_string(),
};
Ok(warp::reply::with_status(
warp::reply::json(&response),
warp::http::StatusCode::OK,
))
}
pub async fn handle_rejection(err: Rejection) -> Result<impl warp::Reply, Rejection> {
if let Some(e) = err.find::<AnyhowRejection>() {
let message = e.0.to_string();

View File

@@ -5,6 +5,7 @@ use crate::handlers::{
end_session_handler, get_provider_config_handler, handle_rejection,
list_extensions_handler, metrics_handler, reply_session_handler,
start_session_handler, summarize_session_handler, with_api_key,
list_sessions_handler, get_session_handler,
};
use crate::config::{
load_provider_config, load_configuration,
@@ -54,6 +55,19 @@ pub fn build_routes(api_key: String) -> impl Filter<Extract = impl warp::Reply,
.and(warp::get())
.and_then(metrics_handler);
let list_sessions = warp::path("sessions")
.and(warp::path("list"))
.and(warp::get())
.and(with_api_key(api_key.clone()))
.and_then(list_sessions_handler);
let get_session = warp::path("session")
.and(warp::path("get"))
.and(warp::post())
.and(warp::body::json())
.and(with_api_key(api_key.clone()))
.and_then(get_session_handler);
start_session
.or(reply_session)
.or(summarize_session)
@@ -61,6 +75,8 @@ pub fn build_routes(api_key: String) -> impl Filter<Extract = impl warp::Reply,
.or(list_extensions)
.or(get_provider_config)
.or(metrics)
.or(list_sessions)
.or(get_session)
.recover(handle_rejection)
}

View File

@@ -1324,7 +1324,17 @@ pub async fn generate_description_with_schedule_id(
anyhow::anyhow!("Failed to generate session description")
})?;
let description = result.0.as_concat_text();
// Extract only non-thinking text content for the description
let description = result.0.content
.iter()
.filter_map(|c| match c {
crate::message::MessageContent::Text(text) => Some(text.text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n")
.trim()
.to_string();
// Validate description length for security
let sanitized_description = if description.chars().count() > 100 {