From 2faba0388f93d8e4fb86219eba7883b331d501ff Mon Sep 17 00:00:00 2001 From: soryu Date: Wed, 24 Dec 2025 05:45:22 +0000 Subject: Add versioning to files --- makima/src/server/handlers/chat.rs | 246 ++++++++++++++++++++++++++++++++++++- 1 file changed, 241 insertions(+), 5 deletions(-) (limited to 'makima/src/server/handlers/chat.rs') diff --git a/makima/src/server/handlers/chat.rs b/makima/src/server/handlers/chat.rs index 3bdbc74..396c973 100644 --- a/makima/src/server/handlers/chat.rs +++ b/makima/src/server/handlers/chat.rs @@ -10,12 +10,12 @@ use serde::{Deserialize, Serialize}; use utoipa::ToSchema; use uuid::Uuid; -use crate::db::{models::BodyElement, repository}; +use crate::db::{models::BodyElement, repository::{self, RepositoryError}}; use crate::llm::{ claude::{self, ClaudeClient, ClaudeError, ClaudeModel}, execute_tool_call, groq::{GroqClient, GroqError, Message, ToolCallResponse}, - LlmModel, ToolCall, ToolResult, AVAILABLE_TOOLS, + LlmModel, ToolCall, ToolResult, VersionToolRequest, AVAILABLE_TOOLS, }; use crate::server::state::{FileUpdateNotification, SharedState}; @@ -236,6 +236,10 @@ pub async fn chat_handler( let mut current_summary = file.summary.clone(); let mut all_tool_call_infos: Vec = Vec::new(); let mut final_response: Option = None; + // Track if a version restore already happened (to avoid double-saving) + let mut version_restored = false; + // Track if there were modifications after a restore + let mut has_changes_after_restore = false; // Multi-turn tool calling loop for round in 0..MAX_TOOL_ROUNDS { @@ -320,15 +324,49 @@ pub async fn chat_handler( // Execute each tool call and add results to conversation for (i, tool_call) in result.tool_calls.iter().enumerate() { - let execution_result = + let mut execution_result = execute_tool_call(tool_call, ¤t_body, current_summary.as_deref()); - // Apply state changes + // Handle version tool requests that need async database access + if let Some(version_request) = &execution_result.version_request { + let version_result = handle_version_request( + pool, + id, + version_request, + ¤t_body, + current_summary.as_deref(), + file.version, + ) + .await; + + // Update execution result with actual version operation result + execution_result.result = version_result.result; + execution_result.parsed_data = version_result.data; + + // Apply state changes from restore operation + if let Some(new_body) = version_result.new_body { + current_body = new_body; + // Mark that a restore happened - file was already saved + version_restored = true; + } + if let Some(new_summary) = version_result.new_summary { + current_summary = Some(new_summary); + } + } + + // Apply state changes from regular tools if let Some(new_body) = execution_result.new_body { current_body = new_body; + // If this is a regular tool (not a version operation), track it + if execution_result.version_request.is_none() && version_restored { + has_changes_after_restore = true; + } } if let Some(new_summary) = execution_result.new_summary { current_summary = Some(new_summary); + if execution_result.version_request.is_none() && version_restored { + has_changes_after_restore = true; + } } // Build tool result message content @@ -378,7 +416,9 @@ pub async fn chat_handler( } // Save changes to database if any tools were executed - if !all_tool_call_infos.is_empty() { + // Skip if a version restore already happened (file was already saved during restore) + // UNLESS there were additional modifications after the restore + if !all_tool_call_infos.is_empty() && (!version_restored || has_changes_after_restore) { let update_req = crate::db::models::UpdateFileRequest { name: None, description: None, @@ -506,3 +546,199 @@ fn build_file_context(file: &crate::db::models::File) -> String { context } + +/// Result of handling a version tool request +struct VersionRequestResult { + result: ToolResult, + data: Option, + new_body: Option>, + new_summary: Option, +} + +/// Handle version tool requests that require async database access +async fn handle_version_request( + pool: &sqlx::PgPool, + file_id: Uuid, + request: &VersionToolRequest, + _current_body: &[BodyElement], + _current_summary: Option<&str>, + current_version: i32, +) -> VersionRequestResult { + match request { + VersionToolRequest::ListVersions => { + match repository::list_file_versions(pool, file_id).await { + Ok(versions) => { + let version_data: Vec = versions + .iter() + .map(|v| { + serde_json::json!({ + "version": v.version, + "source": v.source, + "createdAt": v.created_at.to_rfc3339(), + "changeDescription": v.change_description, + }) + }) + .collect(); + + VersionRequestResult { + result: ToolResult { + success: true, + message: format!("Found {} versions. Current version is {}.", versions.len(), current_version), + }, + data: Some(serde_json::json!({ + "currentVersion": current_version, + "versions": version_data, + })), + new_body: None, + new_summary: None, + } + } + Err(e) => VersionRequestResult { + result: ToolResult { + success: false, + message: format!("Failed to list versions: {}", e), + }, + data: None, + new_body: None, + new_summary: None, + }, + } + } + VersionToolRequest::ReadVersion { version } => { + match repository::get_file_version(pool, file_id, *version).await { + Ok(Some(ver)) => { + // Convert body elements to a readable format + let body_preview: Vec = ver + .body + .iter() + .enumerate() + .map(|(i, element)| { + let desc = match element { + BodyElement::Heading { level, text } => format!("H{}: {}", level, text), + BodyElement::Paragraph { text } => { + let preview = if text.len() > 100 { + format!("{}...", &text[..100]) + } else { + text.clone() + }; + format!("Paragraph: {}", preview) + } + BodyElement::Chart { chart_type, title, .. } => { + format!( + "Chart ({:?}){}", + chart_type, + title.as_ref().map(|t| format!(": {}", t)).unwrap_or_default() + ) + } + BodyElement::Image { alt, .. } => { + format!("Image{}", alt.as_ref().map(|a| format!(": {}", a)).unwrap_or_default()) + } + }; + format!("[{}] {}", i, desc) + }) + .collect(); + + VersionRequestResult { + result: ToolResult { + success: true, + message: format!( + "Version {} from {} (source: {}). {} body elements.", + ver.version, + ver.created_at.format("%Y-%m-%d %H:%M"), + ver.source, + ver.body.len() + ), + }, + data: Some(serde_json::json!({ + "version": ver.version, + "source": ver.source, + "createdAt": ver.created_at.to_rfc3339(), + "summary": ver.summary, + "bodyPreview": body_preview, + "changeDescription": ver.change_description, + })), + new_body: None, + new_summary: None, + } + } + Ok(None) => VersionRequestResult { + result: ToolResult { + success: false, + message: format!("Version {} not found", version), + }, + data: None, + new_body: None, + new_summary: None, + }, + Err(e) => VersionRequestResult { + result: ToolResult { + success: false, + message: format!("Failed to read version: {}", e), + }, + data: None, + new_body: None, + new_summary: None, + }, + } + } + VersionToolRequest::RestoreVersion { target_version, reason } => { + // Set change description if provided + if let Some(reason) = reason { + let _ = repository::set_change_description(pool, reason).await; + } + + match repository::restore_file_version(pool, file_id, *target_version, current_version).await { + Ok(Some(restored_file)) => { + VersionRequestResult { + result: ToolResult { + success: true, + message: format!( + "Restored to version {}. New version is {}.", + target_version, restored_file.version + ), + }, + data: Some(serde_json::json!({ + "previousVersion": current_version, + "restoredFromVersion": target_version, + "newVersion": restored_file.version, + })), + new_body: Some(restored_file.body), + new_summary: restored_file.summary, + } + } + Ok(None) => VersionRequestResult { + result: ToolResult { + success: false, + message: format!("Version {} not found", target_version), + }, + data: None, + new_body: None, + new_summary: None, + }, + Err(RepositoryError::VersionConflict { expected, actual }) => { + VersionRequestResult { + result: ToolResult { + success: false, + message: format!( + "Version conflict: expected {}, actual {}. Document was modified.", + expected, actual + ), + }, + data: None, + new_body: None, + new_summary: None, + } + } + Err(e) => VersionRequestResult { + result: ToolResult { + success: false, + message: format!("Failed to restore version: {}", e), + }, + data: None, + new_body: None, + new_summary: None, + }, + } + } + } +} -- cgit v1.2.3