summaryrefslogblamecommitdiff
path: root/makima/src/llm/tools.rs
blob: 35f321fba307ba551f711b66748898a866ec1eb4 (plain) (tree)
1
2
3

                                              
                           
























































































































                                                                                                                                               
                                                                                                                                                                                                                                                                                   






                                                                                   

























                                                                                              

                         
                                                         























































                                                                                               
















                                                                                                                                                                                                                         









































                                                                                                                                                                                                                           


         










                                                                             






                                                            

                                                                      

















                                                                           
                                 



                                                                    







                                                                
                                  



































                                                                                              
                              







































                                                                                                
                              
































































                                                                                            
                              














                                                                                                 
                                  












                                                                                                             
                                  













                                                                   
                              




                                                                                                 
                                                                                   









                                                               
                                  


          
                                                


                                    
                                                                      



                              
                                  












                                                                                                             
                                  


          


























                                                                                                           
                                    

                                                                                                                      
                  
                               

                                  
                                      
              
         








                                                                                       
          


                                 
                              















                                                                                                   
                                  

















                                                                                
                                  














                                                                            
                              


















                                                                                                
                              



















                                                              
                                  


































                                                                             
                              











                                                             
                              

     












                                                                   
                                      














                                                                              
                                      


















                                                                            
                                  











                                                                 
                                  













                                                                                             
                                  























                                                                                   
                                          




























                                                                   



















































































                                                                                           





























































                                                                                                          
//! Tool definitions for file editing via LLM.

use jaq_interpret::FilterT;
use serde::{Deserialize, Serialize};
use serde_json::json;

use crate::db::models::{BodyElement, ChartType};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tool {
    pub name: String,
    pub description: String,
    pub parameters: serde_json::Value,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
    pub id: String,
    pub name: String,
    pub arguments: serde_json::Value,
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct ToolResult {
    pub success: bool,
    pub message: String,
}

/// Available tools for file editing
pub static AVAILABLE_TOOLS: once_cell::sync::Lazy<Vec<Tool>> =
    once_cell::sync::Lazy::new(|| {
        vec![
            Tool {
                name: "add_heading".to_string(),
                description: "Add a heading element to the file body".to_string(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "level": {
                            "type": "integer",
                            "description": "Heading level (1-6)",
                            "minimum": 1,
                            "maximum": 6
                        },
                        "text": {
                            "type": "string",
                            "description": "The heading text"
                        },
                        "position": {
                            "type": "integer",
                            "description": "Optional position to insert at (0-indexed). If not specified, appends to end."
                        }
                    },
                    "required": ["level", "text"]
                }),
            },
            Tool {
                name: "add_paragraph".to_string(),
                description: "Add a paragraph element to the file body".to_string(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "text": {
                            "type": "string",
                            "description": "The paragraph text"
                        },
                        "position": {
                            "type": "integer",
                            "description": "Optional position to insert at (0-indexed). If not specified, appends to end."
                        }
                    },
                    "required": ["text"]
                }),
            },
            Tool {
                name: "add_chart".to_string(),
                description: "Add a chart visualization to the file body. Supports line, bar, pie, and area charts.".to_string(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "chart_type": {
                            "type": "string",
                            "enum": ["line", "bar", "pie", "area"],
                            "description": "Type of chart to create"
                        },
                        "title": {
                            "type": "string",
                            "description": "Optional chart title"
                        },
                        "data": {
                            "type": "array",
                            "description": "Array of data points. Each point should have a 'name' field and one or more numeric value fields.",
                            "items": {
                                "type": "object"
                            }
                        },
                        "config": {
                            "type": "object",
                            "description": "Optional chart configuration (colors, axes, etc.)"
                        },
                        "position": {
                            "type": "integer",
                            "description": "Optional position to insert at (0-indexed). If not specified, appends to end."
                        }
                    },
                    "required": ["chart_type", "data"]
                }),
            },
            Tool {
                name: "remove_element".to_string(),
                description: "Remove an element from the file body by index".to_string(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "index": {
                            "type": "integer",
                            "description": "Index of element to remove (0-indexed)"
                        }
                    },
                    "required": ["index"]
                }),
            },
            Tool {
                name: "update_element".to_string(),
                description: "Update an existing element in the file body. IMPORTANT: You must provide ALL required fields. For heading: type, level (1-6), text. For paragraph: type, text. For chart: type, chartType (line/bar/pie/area), data (array of objects).".to_string(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "index": {
                            "type": "integer",
                            "description": "Index of element to update (0-indexed)"
                        },
                        "element_type": {
                            "type": "string",
                            "enum": ["heading", "paragraph", "chart"],
                            "description": "Type of element"
                        },
                        "text": {
                            "type": "string",
                            "description": "Text content (required for heading and paragraph)"
                        },
                        "level": {
                            "type": "integer",
                            "description": "Heading level 1-6 (required for heading)"
                        },
                        "chartType": {
                            "type": "string",
                            "enum": ["line", "bar", "pie", "area"],
                            "description": "Chart type (required for chart)"
                        },
                        "data": {
                            "type": "array",
                            "description": "Chart data array (required for chart)",
                            "items": { "type": "object" }
                        },
                        "title": {
                            "type": "string",
                            "description": "Chart title (optional for chart)"
                        }
                    },
                    "required": ["index", "element_type"]
                }),
            },
            Tool {
                name: "reorder_elements".to_string(),
                description: "Move an element from one position to another".to_string(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "from_index": {
                            "type": "integer",
                            "description": "Current index of the element"
                        },
                        "to_index": {
                            "type": "integer",
                            "description": "New index for the element"
                        }
                    },
                    "required": ["from_index", "to_index"]
                }),
            },
            Tool {
                name: "set_summary".to_string(),
                description: "Set the file summary text".to_string(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "summary": {
                            "type": "string",
                            "description": "The summary text"
                        }
                    },
                    "required": ["summary"]
                }),
            },
            Tool {
                name: "parse_csv".to_string(),
                description: "Parse CSV data into JSON format suitable for charts".to_string(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "csv": {
                            "type": "string",
                            "description": "CSV data string with header row"
                        }
                    },
                    "required": ["csv"]
                }),
            },
            Tool {
                name: "clear_body".to_string(),
                description: "Clear all elements from the file body".to_string(),
                parameters: json!({
                    "type": "object",
                    "properties": {}
                }),
            },
            Tool {
                name: "jq".to_string(),
                description: "Transform JSON data using jq expressions. Useful for filtering, mapping, grouping, and aggregating data before creating charts.".to_string(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "input": {
                            "description": "The JSON data to transform (can be an array or object)"
                        },
                        "filter": {
                            "type": "string",
                            "description": "The jq filter expression. Examples: '.[] | select(.value > 10)', 'group_by(.category) | map({name: .[0].category, count: length})', '[.[] | {name: .label, value: .amount}]'"
                        }
                    },
                    "required": ["input", "filter"]
                }),
            },
            // Version history tools
            Tool {
                name: "list_versions".to_string(),
                description: "List all available versions of the current document. Returns version numbers, sources (user/llm/system), timestamps, and change descriptions.".to_string(),
                parameters: json!({
                    "type": "object",
                    "properties": {},
                    "required": []
                }),
            },
            Tool {
                name: "read_version".to_string(),
                description: "Read the content of a specific historical version of the document. This is read-only and does not modify the current document.".to_string(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "version": {
                            "type": "integer",
                            "description": "The version number to read"
                        }
                    },
                    "required": ["version"]
                }),
            },
            Tool {
                name: "restore_version".to_string(),
                description: "Restore the document to a previous version. This creates a new version with the content from the target version. The current content will be preserved as a historical version.".to_string(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "target_version": {
                            "type": "integer",
                            "description": "The version number to restore to"
                        },
                        "reason": {
                            "type": "string",
                            "description": "Optional reason for the restore (will be recorded in change description)"
                        }
                    },
                    "required": ["target_version"]
                }),
            },
        ]
    });

/// Request for version-related operations that require async database access
#[derive(Debug, Clone)]
pub enum VersionToolRequest {
    /// List all versions of the current file
    ListVersions,
    /// Read a specific version
    ReadVersion { version: i32 },
    /// Restore to a specific version
    RestoreVersion { target_version: i32, reason: Option<String> },
}

/// Result of executing a tool call with modified file state
#[derive(Debug)]
pub struct ToolExecutionResult {
    pub result: ToolResult,
    pub new_body: Option<Vec<BodyElement>>,
    pub new_summary: Option<String>,
    pub parsed_data: Option<serde_json::Value>,
    /// Request for async version operations (handled by chat handler)
    pub version_request: Option<VersionToolRequest>,
}

/// Execute a tool call and return the result along with any state changes
pub fn execute_tool_call(
    call: &ToolCall,
    current_body: &[BodyElement],
    current_summary: Option<&str>,
) -> ToolExecutionResult {
    match call.name.as_str() {
        "add_heading" => execute_add_heading(call, current_body),
        "add_paragraph" => execute_add_paragraph(call, current_body),
        "add_chart" => execute_add_chart(call, current_body),
        "remove_element" => execute_remove_element(call, current_body),
        "update_element" => execute_update_element(call, current_body),
        "reorder_elements" => execute_reorder_elements(call, current_body),
        "set_summary" => execute_set_summary(call, current_summary),
        "parse_csv" => execute_parse_csv(call),
        "clear_body" => execute_clear_body(),
        "jq" => execute_jq(call),
        // Version history tools - return request for async handling
        "list_versions" => execute_list_versions(),
        "read_version" => execute_read_version(call),
        "restore_version" => execute_restore_version(call),
        _ => ToolExecutionResult {
            result: ToolResult {
                success: false,
                message: format!("Unknown tool: {}", call.name),
            },
            new_body: None,
            new_summary: None,
            parsed_data: None,
            version_request: None,
        },
    }
}

fn execute_add_heading(call: &ToolCall, current_body: &[BodyElement]) -> ToolExecutionResult {
    let level = call.arguments.get("level").and_then(|v| v.as_u64()).unwrap_or(1) as u8;
    let text = call
        .arguments
        .get("text")
        .and_then(|v| v.as_str())
        .unwrap_or("")
        .to_string();
    let position = call.arguments.get("position").and_then(|v| v.as_u64());

    let element = BodyElement::Heading { level, text: text.clone() };
    let mut new_body = current_body.to_vec();

    if let Some(pos) = position {
        let pos = pos as usize;
        if pos <= new_body.len() {
            new_body.insert(pos, element);
        } else {
            new_body.push(element);
        }
    } else {
        new_body.push(element);
    }

    ToolExecutionResult {
        result: ToolResult {
            success: true,
            message: format!("Added heading: {}", text),
        },
        new_body: Some(new_body),
        new_summary: None,
        parsed_data: None,
        version_request: None,
    }
}

fn execute_add_paragraph(call: &ToolCall, current_body: &[BodyElement]) -> ToolExecutionResult {
    let text = call
        .arguments
        .get("text")
        .and_then(|v| v.as_str())
        .unwrap_or("")
        .to_string();
    let position = call.arguments.get("position").and_then(|v| v.as_u64());

    let element = BodyElement::Paragraph { text: text.clone() };
    let mut new_body = current_body.to_vec();

    if let Some(pos) = position {
        let pos = pos as usize;
        if pos <= new_body.len() {
            new_body.insert(pos, element);
        } else {
            new_body.push(element);
        }
    } else {
        new_body.push(element);
    }

    let preview = if text.len() > 50 {
        format!("{}...", &text[..50])
    } else {
        text
    };

    ToolExecutionResult {
        result: ToolResult {
            success: true,
            message: format!("Added paragraph: {}", preview),
        },
        new_body: Some(new_body),
        new_summary: None,
        parsed_data: None,
        version_request: None,
    }
}

fn execute_add_chart(call: &ToolCall, current_body: &[BodyElement]) -> ToolExecutionResult {
    let chart_type_str = call
        .arguments
        .get("chart_type")
        .and_then(|v| v.as_str())
        .unwrap_or("bar");

    let chart_type = match chart_type_str {
        "line" => ChartType::Line,
        "bar" => ChartType::Bar,
        "pie" => ChartType::Pie,
        "area" => ChartType::Area,
        _ => ChartType::Bar,
    };

    let title = call
        .arguments
        .get("title")
        .and_then(|v| v.as_str())
        .map(|s| s.to_string());

    let data = call
        .arguments
        .get("data")
        .cloned()
        .unwrap_or(json!([]));

    let config = call.arguments.get("config").cloned();
    let position = call.arguments.get("position").and_then(|v| v.as_u64());

    let element = BodyElement::Chart {
        chart_type,
        title: title.clone(),
        data,
        config,
    };

    let mut new_body = current_body.to_vec();

    if let Some(pos) = position {
        let pos = pos as usize;
        if pos <= new_body.len() {
            new_body.insert(pos, element);
        } else {
            new_body.push(element);
        }
    } else {
        new_body.push(element);
    }

    ToolExecutionResult {
        result: ToolResult {
            success: true,
            message: format!(
                "Added {} chart{}",
                chart_type_str,
                title.map(|t| format!(": {}", t)).unwrap_or_default()
            ),
        },
        new_body: Some(new_body),
        new_summary: None,
        parsed_data: None,
        version_request: None,
    }
}

fn execute_remove_element(call: &ToolCall, current_body: &[BodyElement]) -> ToolExecutionResult {
    let index = call.arguments.get("index").and_then(|v| v.as_u64());

    let Some(index) = index else {
        return ToolExecutionResult {
            result: ToolResult {
                success: false,
                message: "Missing index parameter".to_string(),
            },
            new_body: None,
            new_summary: None,
            parsed_data: None,
            version_request: None,
        };
    };

    let index = index as usize;
    if index >= current_body.len() {
        return ToolExecutionResult {
            result: ToolResult {
                success: false,
                message: format!("Index {} out of bounds (body has {} elements)", index, current_body.len()),
            },
            new_body: None,
            new_summary: None,
            parsed_data: None,
            version_request: None,
        };
    }

    let mut new_body = current_body.to_vec();
    new_body.remove(index);

    ToolExecutionResult {
        result: ToolResult {
            success: true,
            message: format!("Removed element at index {}", index),
        },
        new_body: Some(new_body),
        new_summary: None,
        parsed_data: None,
        version_request: None,
    }
}

fn execute_update_element(call: &ToolCall, current_body: &[BodyElement]) -> ToolExecutionResult {
    let index = call.arguments.get("index").and_then(|v| v.as_u64());
    let element_type = call.arguments.get("element_type").and_then(|v| v.as_str());

    let Some(index) = index else {
        return ToolExecutionResult {
            result: ToolResult {
                success: false,
                message: "Missing index parameter".to_string(),
            },
            new_body: None,
            new_summary: None,
            parsed_data: None,
            version_request: None,
        };
    };

    let Some(element_type) = element_type else {
        return ToolExecutionResult {
            result: ToolResult {
                success: false,
                message: "Missing element_type parameter".to_string(),
            },
            new_body: None,
            new_summary: None,
            parsed_data: None,
            version_request: None,
        };
    };

    let index = index as usize;
    if index >= current_body.len() {
        return ToolExecutionResult {
            result: ToolResult {
                success: false,
                message: format!("Index {} out of bounds (body has {} elements)", index, current_body.len()),
            },
            new_body: None,
            new_summary: None,
            parsed_data: None,
            version_request: None,
        };
    }

    // Build the element based on type
    let new_element = match element_type {
        "heading" => {
            let level = call.arguments.get("level").and_then(|v| v.as_u64()).unwrap_or(1) as u8;
            let text = call.arguments.get("text").and_then(|v| v.as_str()).unwrap_or("").to_string();
            BodyElement::Heading { level, text }
        }
        "paragraph" => {
            let text = call.arguments.get("text").and_then(|v| v.as_str()).unwrap_or("").to_string();
            BodyElement::Paragraph { text }
        }
        "chart" => {
            let chart_type_str = call.arguments.get("chartType").and_then(|v| v.as_str()).unwrap_or("bar");
            let chart_type = match chart_type_str {
                "line" => ChartType::Line,
                "bar" => ChartType::Bar,
                "pie" => ChartType::Pie,
                "area" => ChartType::Area,
                _ => ChartType::Bar,
            };
            let title = call.arguments.get("title").and_then(|v| v.as_str()).map(|s| s.to_string());
            let data = call.arguments.get("data").cloned().unwrap_or(json!([]));
            let config = call.arguments.get("config").cloned();
            BodyElement::Chart { chart_type, title, data, config }
        }
        _ => {
            return ToolExecutionResult {
                result: ToolResult {
                    success: false,
                    message: format!("Unknown element_type: {}. Must be heading, paragraph, or chart.", element_type),
                },
                new_body: None,
                new_summary: None,
                parsed_data: None,
                version_request: None,
            };
        }
    };

    let mut new_body = current_body.to_vec();
    new_body[index] = new_element;

    ToolExecutionResult {
        result: ToolResult {
            success: true,
            message: format!("Updated element at index {} to {}", index, element_type),
        },
        new_body: Some(new_body),
        new_summary: None,
        parsed_data: None,
        version_request: None,
    }
}

fn execute_reorder_elements(call: &ToolCall, current_body: &[BodyElement]) -> ToolExecutionResult {
    let from_index = call.arguments.get("from_index").and_then(|v| v.as_u64());
    let to_index = call.arguments.get("to_index").and_then(|v| v.as_u64());

    let (Some(from), Some(to)) = (from_index, to_index) else {
        return ToolExecutionResult {
            result: ToolResult {
                success: false,
                message: "Missing from_index or to_index parameter".to_string(),
            },
            new_body: None,
            new_summary: None,
            parsed_data: None,
            version_request: None,
        };
    };

    let from = from as usize;
    let to = to as usize;

    if from >= current_body.len() || to >= current_body.len() {
        return ToolExecutionResult {
            result: ToolResult {
                success: false,
                message: format!(
                    "Index out of bounds: from={}, to={}, body has {} elements",
                    from, to, current_body.len()
                ),
            },
            new_body: None,
            new_summary: None,
            parsed_data: None,
            version_request: None,
        };
    }

    let mut new_body = current_body.to_vec();
    let element = new_body.remove(from);
    new_body.insert(to, element);

    ToolExecutionResult {
        result: ToolResult {
            success: true,
            message: format!("Moved element from index {} to {}", from, to),
        },
        new_body: Some(new_body),
        new_summary: None,
        parsed_data: None,
        version_request: None,
    }
}

fn execute_set_summary(call: &ToolCall, _current_summary: Option<&str>) -> ToolExecutionResult {
    let summary = call
        .arguments
        .get("summary")
        .and_then(|v| v.as_str())
        .unwrap_or("")
        .to_string();

    ToolExecutionResult {
        result: ToolResult {
            success: true,
            message: "Summary updated".to_string(),
        },
        new_body: None,
        new_summary: Some(summary),
        parsed_data: None,
        version_request: None,
    }
}

fn execute_parse_csv(call: &ToolCall) -> ToolExecutionResult {
    let csv = call
        .arguments
        .get("csv")
        .and_then(|v| v.as_str())
        .unwrap_or("");

    let lines: Vec<&str> = csv.lines().collect();
    if lines.is_empty() {
        return ToolExecutionResult {
            result: ToolResult {
                success: false,
                message: "Empty CSV data".to_string(),
            },
            new_body: None,
            new_summary: None,
            parsed_data: None,
            version_request: None,
        };
    }

    let headers: Vec<&str> = lines[0].split(',').map(|s| s.trim()).collect();
    let mut data: Vec<serde_json::Value> = Vec::new();

    for line in lines.iter().skip(1) {
        if line.trim().is_empty() {
            continue;
        }
        let values: Vec<&str> = line.split(',').map(|s| s.trim()).collect();
        let mut row = serde_json::Map::new();

        for (i, header) in headers.iter().enumerate() {
            if let Some(value) = values.get(i) {
                // Try to parse as number, otherwise use string
                if let Ok(num) = value.parse::<f64>() {
                    row.insert(header.to_string(), json!(num));
                } else {
                    row.insert(header.to_string(), json!(value));
                }
            }
        }

        data.push(serde_json::Value::Object(row));
    }

    ToolExecutionResult {
        result: ToolResult {
            success: true,
            message: format!("Parsed {} rows from CSV", data.len()),
        },
        new_body: None,
        new_summary: None,
        parsed_data: Some(json!(data)),
        version_request: None,
    }
}

fn execute_clear_body() -> ToolExecutionResult {
    ToolExecutionResult {
        result: ToolResult {
            success: true,
            message: "Cleared all body elements".to_string(),
        },
        new_body: Some(vec![]),
        new_summary: None,
        parsed_data: None,
        version_request: None,
    }
}

fn execute_jq(call: &ToolCall) -> ToolExecutionResult {
    let input = match call.arguments.get("input") {
        Some(v) => v.clone(),
        None => {
            return ToolExecutionResult {
                result: ToolResult {
                    success: false,
                    message: "Missing input parameter".to_string(),
                },
                new_body: None,
                new_summary: None,
                parsed_data: None,
                version_request: None,
            };
        }
    };

    let filter = match call.arguments.get("filter").and_then(|v| v.as_str()) {
        Some(f) => f,
        None => {
            return ToolExecutionResult {
                result: ToolResult {
                    success: false,
                    message: "Missing filter parameter".to_string(),
                },
                new_body: None,
                new_summary: None,
                parsed_data: None,
                version_request: None,
            };
        }
    };

    // Parse the jq filter
    let mut defs = jaq_interpret::ParseCtx::new(Vec::new());
    defs.insert_natives(jaq_core::core());
    defs.insert_defs(jaq_std::std());

    let (parsed_filter, errs) = jaq_parse::parse(filter, jaq_parse::main());
    if !errs.is_empty() {
        return ToolExecutionResult {
            result: ToolResult {
                success: false,
                message: format!("Invalid jq filter: {:?}", errs),
            },
            new_body: None,
            new_summary: None,
            parsed_data: None,
            version_request: None,
        };
    }

    let Some(parsed_filter) = parsed_filter else {
        return ToolExecutionResult {
            result: ToolResult {
                success: false,
                message: "Failed to parse jq filter".to_string(),
            },
            new_body: None,
            new_summary: None,
            parsed_data: None,
            version_request: None,
        };
    };

    // Compile the filter
    let compiled = defs.compile(parsed_filter);
    if !defs.errs.is_empty() {
        return ToolExecutionResult {
            result: ToolResult {
                success: false,
                message: format!("Failed to compile jq filter ({} errors)", defs.errs.len()),
            },
            new_body: None,
            new_summary: None,
            parsed_data: None,
            version_request: None,
        };
    }

    // Convert serde_json::Value to jaq Value
    let jaq_input = json_to_jaq(&input);

    // Execute the filter
    let inputs = jaq_interpret::RcIter::new(std::iter::empty());
    let mut results: Vec<serde_json::Value> = Vec::new();

    for output in compiled.run((jaq_interpret::Ctx::new([], &inputs), jaq_input)) {
        match output {
            Ok(val) => {
                results.push(jaq_to_json(&val));
            }
            Err(e) => {
                return ToolExecutionResult {
                    result: ToolResult {
                        success: false,
                        message: format!("jq execution error: {:?}", e),
                    },
                    new_body: None,
                    new_summary: None,
                    parsed_data: None,
                    version_request: None,
                };
            }
        }
    }

    // Return single value or array based on results
    let output = if results.len() == 1 {
        results.into_iter().next().unwrap()
    } else {
        json!(results)
    };

    let preview = {
        let s = output.to_string();
        if s.len() > 100 {
            format!("{}...", &s[..100])
        } else {
            s
        }
    };

    ToolExecutionResult {
        result: ToolResult {
            success: true,
            message: format!("jq transform complete: {}", preview),
        },
        new_body: None,
        new_summary: None,
        parsed_data: Some(output),
        version_request: None,
    }
}

// =============================================================================
// Version History Tool Execution Functions
// =============================================================================
// These return version_request instead of performing the operation directly,
// because they require async database access which is handled in the chat handler.

fn execute_list_versions() -> ToolExecutionResult {
    ToolExecutionResult {
        result: ToolResult {
            success: true,
            message: "Listing versions...".to_string(),
        },
        new_body: None,
        new_summary: None,
        parsed_data: None,
        version_request: Some(VersionToolRequest::ListVersions),
    }
}

fn execute_read_version(call: &ToolCall) -> ToolExecutionResult {
    let version = call.arguments.get("version").and_then(|v| v.as_i64());

    let Some(version) = version else {
        return ToolExecutionResult {
            result: ToolResult {
                success: false,
                message: "Missing version parameter".to_string(),
            },
            new_body: None,
            new_summary: None,
            parsed_data: None,
            version_request: None,
        };
    };

    ToolExecutionResult {
        result: ToolResult {
            success: true,
            message: format!("Reading version {}...", version),
        },
        new_body: None,
        new_summary: None,
        parsed_data: None,
        version_request: Some(VersionToolRequest::ReadVersion { version: version as i32 }),
    }
}

fn execute_restore_version(call: &ToolCall) -> ToolExecutionResult {
    let target_version = call.arguments.get("target_version").and_then(|v| v.as_i64());
    let reason = call
        .arguments
        .get("reason")
        .and_then(|v| v.as_str())
        .map(|s| s.to_string());

    let Some(target_version) = target_version else {
        return ToolExecutionResult {
            result: ToolResult {
                success: false,
                message: "Missing target_version parameter".to_string(),
            },
            new_body: None,
            new_summary: None,
            parsed_data: None,
            version_request: None,
        };
    };

    ToolExecutionResult {
        result: ToolResult {
            success: true,
            message: format!("Restoring to version {}...", target_version),
        },
        new_body: None,
        new_summary: None,
        parsed_data: None,
        version_request: Some(VersionToolRequest::RestoreVersion {
            target_version: target_version as i32,
            reason,
        }),
    }
}

/// Convert serde_json::Value to jaq_interpret::Val
fn json_to_jaq(value: &serde_json::Value) -> jaq_interpret::Val {
    match value {
        serde_json::Value::Null => jaq_interpret::Val::Null,
        serde_json::Value::Bool(b) => jaq_interpret::Val::Bool(*b),
        serde_json::Value::Number(n) => {
            if let Some(i) = n.as_i64() {
                jaq_interpret::Val::Int(i as isize)
            } else if let Some(f) = n.as_f64() {
                jaq_interpret::Val::Float(f)
            } else {
                jaq_interpret::Val::Null
            }
        }
        serde_json::Value::String(s) => jaq_interpret::Val::Str(s.clone().into()),
        serde_json::Value::Array(arr) => {
            jaq_interpret::Val::Arr(std::rc::Rc::new(arr.iter().map(json_to_jaq).collect()))
        }
        serde_json::Value::Object(obj) => {
            let mut map: indexmap::IndexMap<std::rc::Rc<String>, jaq_interpret::Val, ahash::RandomState> =
                indexmap::IndexMap::with_hasher(ahash::RandomState::new());
            for (k, v) in obj {
                map.insert(std::rc::Rc::new(k.clone()), json_to_jaq(v));
            }
            jaq_interpret::Val::Obj(std::rc::Rc::new(map))
        }
    }
}

/// Convert jaq_interpret::Val to serde_json::Value
fn jaq_to_json(value: &jaq_interpret::Val) -> serde_json::Value {
    match value {
        jaq_interpret::Val::Null => serde_json::Value::Null,
        jaq_interpret::Val::Bool(b) => json!(*b),
        jaq_interpret::Val::Int(i) => json!(*i),
        jaq_interpret::Val::Float(f) => json!(*f),
        jaq_interpret::Val::Num(n) => {
            // Try to parse the number string
            if let Ok(i) = n.parse::<i64>() {
                json!(i)
            } else if let Ok(f) = n.parse::<f64>() {
                json!(f)
            } else {
                json!(n.as_ref())
            }
        }
        jaq_interpret::Val::Str(s) => json!(s.as_ref()),
        jaq_interpret::Val::Arr(arr) => {
            json!(arr.iter().map(jaq_to_json).collect::<Vec<_>>())
        }
        jaq_interpret::Val::Obj(obj) => {
            let mut map = serde_json::Map::new();
            for (k, v) in obj.iter() {
                map.insert((**k).clone(), jaq_to_json(v));
            }
            serde_json::Value::Object(map)
        }
    }
}