summaryrefslogblamecommitdiff
path: root/makima/src/llm/tools.rs
blob: ae1dc5ab6a72e60eadf39eef49d6fe5434e0ea72 (plain) (tree)
1
2
3
4
5
6
7
8

                                              
                           


                                    
                                                                 
                          



































































                                                                                                                          












































                                                                                                                                         
















































                                                                                                                                               
                                                                                                                                                                                                                                                                                                                                                                                              






                                                                                   

                                             
                                                                                      









                                                                                              
















                                                                                                             












                                                                                   

                         
                                                         























































                                                                                               
















                                                                                                                                                                                                                         









































                                                                                                                                                                                                                                                                                                                           
































                                                                                                                                                                                                          









































                                                                                                                                                                                                                           





























                                                                                                                                                                                                                                                                                  


         










                                                                             































                                                                 






                                                            

                                                                      

                                                                      






                                                                          
                                   



                                                                     

                                                           






                                                                           
                                 

                                             



                                                                   



                                                                    


                                                               







                                                                
                                  







































































                                                                                                    
          




                                           


































                                                                                              
                              
                                







































                                                                                                
                              
                                


     
































































































                                                                                           





























































                                                                                            
                              
                                














                                                                                                 
                                  
                                    












                                                                                                             
                                  
                                    













                                                                   
                              
                                




                                                                                                 
                                                                                   









                                                               
                                  
                                    


          
                                                


                                    
                                                                      



                              
                                  
                                    












                                                                                                             
                                  
                                    


          










                                                                                                     












                                                                                                           















                                                                                                           
                                    
                                   
                                                                                                                                  
                  
                               

                                  
                                      
                                        
              
         








                                                                                       
          


                                 
                              
                                















                                                                                                   
                                  
                                    

















                                                                                
                                  
                                    














                                                                            
                              
                                


















                                                                                                
                              
                                



















                                                              
                                  
                                    


































                                                                             
                              
                                











                                                             
                              
                                

     












                                                                   
                                      
                                        














                                                                              
                                      
                                        


















                                                                            
                                  
                                    











                                                                 
                                  
                                    













                                                                                             
                                  
                                    























                                                                                   
                                          
                                            




























                                                                   
                              
                                



                                                                                













                                                                                
                                    


















                                                               











                                                                  














                                                                                 




                                                            












                                                                                
                                















                                                                                               
                                    













                                                                                                             
                                    















                                                       











                                                          














                                                                         




                                                    




                                                     

                                           

                                             
                                                   










                                                                    
                                













                                                                                   
                                    



































                                                                                                    
                                



                                                                                














                                                                                   
                                















                                                                         
                                    











                                                                                           
                                




















                                                                                       
                                    














                                                                           
                                


     




























































































































                                                                                                  


























































                                                                                                          
//! 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, TranscriptEntry};
use crate::llm::templates;

#[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_code".to_string(),
                description: "Add a code block element to the file body".to_string(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "content": {
                            "type": "string",
                            "description": "The code content"
                        },
                        "language": {
                            "type": "string",
                            "description": "Optional programming language for syntax highlighting (e.g., 'javascript', 'python', 'rust')"
                        },
                        "position": {
                            "type": "integer",
                            "description": "Optional position to insert at (0-indexed). If not specified, appends to end."
                        }
                    },
                    "required": ["content"]
                }),
            },
            Tool {
                name: "add_list".to_string(),
                description: "Add a list element (ordered or unordered) to the file body".to_string(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "items": {
                            "type": "array",
                            "items": { "type": "string" },
                            "description": "Array of list item strings"
                        },
                        "ordered": {
                            "type": "boolean",
                            "description": "If true, creates a numbered list; if false (default), creates a bullet list"
                        },
                        "position": {
                            "type": "integer",
                            "description": "Optional position to insert at (0-indexed). If not specified, appends to end."
                        }
                    },
                    "required": ["items"]
                }),
            },
            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 code: type, content, language (optional). For list: type, items (array of strings), ordered (boolean). 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", "code", "list", "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)"
                        },
                        "content": {
                            "type": "string",
                            "description": "Code content (required for code)"
                        },
                        "language": {
                            "type": "string",
                            "description": "Programming language for syntax highlighting (optional for code)"
                        },
                        "items": {
                            "type": "array",
                            "items": { "type": "string" },
                            "description": "List items (required for list)"
                        },
                        "ordered": {
                            "type": "boolean",
                            "description": "If true, numbered list; if false, bullet list (for list)"
                        },
                        "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"]
                }),
            },
            // Interactive tools
            Tool {
                name: "ask_user".to_string(),
                description: "Ask the user one or more questions. Use this when you need clarification, want to offer choices, or need user input before proceeding. Each question can have multiple choice options and optionally allow custom answers. The conversation will pause until the user responds.".to_string(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "questions": {
                            "type": "array",
                            "description": "List of questions to ask the user",
                            "items": {
                                "type": "object",
                                "properties": {
                                    "id": {
                                        "type": "string",
                                        "description": "Unique identifier for this question (e.g., 'chart_type', 'color_scheme')"
                                    },
                                    "question": {
                                        "type": "string",
                                        "description": "The question to ask the user"
                                    },
                                    "options": {
                                        "type": "array",
                                        "items": { "type": "string" },
                                        "description": "Multiple choice options for the user to select from"
                                    },
                                    "allowMultiple": {
                                        "type": "boolean",
                                        "description": "If true, user can select multiple options. Default false."
                                    },
                                    "allowCustom": {
                                        "type": "boolean",
                                        "description": "If true, user can provide a custom answer instead of selecting from options. Default true."
                                    }
                                },
                                "required": ["id", "question", "options"]
                            }
                        }
                    },
                    "required": ["questions"]
                }),
            },
            // Content viewing tools
            Tool {
                name: "view_body".to_string(),
                description: "View the complete body structure with full content of all elements. Returns detailed information about each element including type, index, and full text/data.".to_string(),
                parameters: json!({
                    "type": "object",
                    "properties": {},
                    "required": []
                }),
            },
            Tool {
                name: "read_element".to_string(),
                description: "Read the full content of a specific body element by its index. Use this to get complete details of a single element.".to_string(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "index": {
                            "type": "integer",
                            "description": "Index of the element to read (0-indexed)"
                        }
                    },
                    "required": ["index"]
                }),
            },
            Tool {
                name: "view_transcript".to_string(),
                description: "View the complete transcript of the file. Returns all transcript entries with speaker names, text, and timestamps.".to_string(),
                parameters: json!({
                    "type": "object",
                    "properties": {},
                    "required": []
                }),
            },
            // 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"]
                }),
            },
            // Template tools
            Tool {
                name: "suggest_templates".to_string(),
                description: "Get suggested file templates based on a contract phase. Returns templates with predefined structures appropriate for research, specify, plan, execute, or review phases. Use this to help users start documents with proper structure.".to_string(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "phase": {
                            "type": "string",
                            "enum": ["research", "specify", "plan", "execute", "review"],
                            "description": "The contract phase to get templates for. If not provided, returns all templates."
                        }
                    },
                    "required": []
                }),
            },
            Tool {
                name: "apply_template".to_string(),
                description: "Apply a template to the current file, replacing the body with the template structure. The template provides a starting structure that should be customized for the user's needs.".to_string(),
                parameters: json!({
                    "type": "object",
                    "properties": {
                        "template_id": {
                            "type": "string",
                            "description": "The template ID to apply (e.g., 'research-notes', 'requirements', 'architecture')"
                        }
                    },
                    "required": ["template_id"]
                }),
            },
        ]
    });

/// 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> },
}

/// A question to ask the user
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct UserQuestion {
    /// Unique identifier for this question
    pub id: String,
    /// The question text
    pub question: String,
    /// Multiple choice options
    pub options: Vec<String>,
    /// Whether multiple options can be selected
    #[serde(default)]
    pub allow_multiple: bool,
    /// Whether a custom answer is allowed
    #[serde(default = "default_allow_custom")]
    pub allow_custom: bool,
}

fn default_allow_custom() -> bool {
    true
}

/// User's answer to a question
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct UserAnswer {
    /// Question ID this answers
    pub id: String,
    /// Selected option(s) or custom answer
    pub answers: Vec<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>,
    /// Questions to ask the user (pauses conversation until answered)
    pub pending_questions: Option<Vec<UserQuestion>>,
}

/// 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>,
    transcript: &[TranscriptEntry],
) -> ToolExecutionResult {
    match call.name.as_str() {
        "add_heading" => execute_add_heading(call, current_body),
        "add_paragraph" => execute_add_paragraph(call, current_body),
        "add_code" => execute_add_code(call, current_body),
        "add_list" => execute_add_list(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),
        // Interactive tools
        "ask_user" => execute_ask_user(call),
        // Content viewing tools
        "view_body" => execute_view_body(current_body),
        "read_element" => execute_read_element(call, current_body),
        "view_transcript" => execute_view_transcript(transcript),
        // 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),
        // Template tools
        "suggest_templates" => execute_suggest_templates(call),
        "apply_template" => execute_apply_template(call),
        _ => ToolExecutionResult {
            result: ToolResult {
                success: false,
                message: format!("Unknown tool: {}", call.name),
            },
            new_body: None,
            new_summary: None,
            parsed_data: None,
            version_request: None,
            pending_questions: None,
        },
    }
}

fn execute_ask_user(call: &ToolCall) -> ToolExecutionResult {
    let questions_value = call.arguments.get("questions");

    let Some(questions_array) = questions_value.and_then(|v| v.as_array()) else {
        return ToolExecutionResult {
            result: ToolResult {
                success: false,
                message: "Missing or invalid 'questions' parameter".to_string(),
            },
            new_body: None,
            new_summary: None,
            parsed_data: None,
            version_request: None,
            pending_questions: None,
        };
    };

    let mut questions: Vec<UserQuestion> = Vec::new();

    for q in questions_array {
        let id = q.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string();
        let question = q.get("question").and_then(|v| v.as_str()).unwrap_or("").to_string();
        let options: Vec<String> = q
            .get("options")
            .and_then(|v| v.as_array())
            .map(|arr| {
                arr.iter()
                    .filter_map(|o| o.as_str())
                    .map(|s| s.to_string())
                    .collect()
            })
            .unwrap_or_default();
        let allow_multiple = q.get("allowMultiple").and_then(|v| v.as_bool()).unwrap_or(false);
        let allow_custom = q.get("allowCustom").and_then(|v| v.as_bool()).unwrap_or(true);

        if id.is_empty() || question.is_empty() || options.is_empty() {
            continue;
        }

        questions.push(UserQuestion {
            id,
            question,
            options,
            allow_multiple,
            allow_custom,
        });
    }

    if questions.is_empty() {
        return ToolExecutionResult {
            result: ToolResult {
                success: false,
                message: "No valid questions provided".to_string(),
            },
            new_body: None,
            new_summary: None,
            parsed_data: None,
            version_request: None,
            pending_questions: None,
        };
    }

    let question_count = questions.len();
    ToolExecutionResult {
        result: ToolResult {
            success: true,
            message: format!("Asking user {} question(s). Waiting for response...", question_count),
        },
        new_body: None,
        new_summary: None,
        parsed_data: None,
        version_request: None,
        pending_questions: Some(questions),
    }
}

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,
        pending_questions: 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,
        pending_questions: None,
    }
}

fn execute_add_code(call: &ToolCall, current_body: &[BodyElement]) -> ToolExecutionResult {
    let language = call
        .arguments
        .get("language")
        .and_then(|v| v.as_str())
        .map(|s| s.to_string());
    let content = call
        .arguments
        .get("content")
        .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::Code {
        language: language.clone(),
        content: content.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 lang_str = language.as_deref().unwrap_or("plain");
    let preview: String = content.chars().take(50).collect();

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

fn execute_add_list(call: &ToolCall, current_body: &[BodyElement]) -> ToolExecutionResult {
    let ordered = call
        .arguments
        .get("ordered")
        .and_then(|v| v.as_bool())
        .unwrap_or(false);
    let items: Vec<String> = call
        .arguments
        .get("items")
        .and_then(|v| v.as_array())
        .map(|arr| {
            arr.iter()
                .filter_map(|v| v.as_str().map(|s| s.to_string()))
                .collect()
        })
        .unwrap_or_default();
    let position = call.arguments.get("position").and_then(|v| v.as_u64());

    let element = BodyElement::List {
        ordered,
        items: items.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 list_type = if ordered { "ordered" } else { "unordered" };

    ToolExecutionResult {
        result: ToolResult {
            success: true,
            message: format!("Added {} list with {} items", list_type, items.len()),
        },
        new_body: Some(new_body),
        new_summary: None,
        parsed_data: None,
        version_request: None,
        pending_questions: 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,
        pending_questions: 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,
            pending_questions: 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,
            pending_questions: 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,
        pending_questions: 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,
            pending_questions: 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,
            pending_questions: 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,
            pending_questions: 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 }
        }
        "code" => {
            let language = call.arguments.get("language").and_then(|v| v.as_str()).map(|s| s.to_string());
            let content = call.arguments.get("content").and_then(|v| v.as_str()).unwrap_or("").to_string();
            BodyElement::Code { language, content }
        }
        "list" => {
            let ordered = call.arguments.get("ordered").and_then(|v| v.as_bool()).unwrap_or(false);
            let items = call.arguments.get("items")
                .and_then(|v| v.as_array())
                .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
                .unwrap_or_default();
            BodyElement::List { ordered, items }
        }
        "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, code, list, or chart.", element_type),
                },
                new_body: None,
                new_summary: None,
                parsed_data: None,
                version_request: None,
                pending_questions: 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,
        pending_questions: 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,
            pending_questions: 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,
            pending_questions: 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,
        pending_questions: 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,
        pending_questions: 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,
            pending_questions: 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,
        pending_questions: 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,
        pending_questions: 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,
                pending_questions: 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,
                pending_questions: 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,
            pending_questions: 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,
            pending_questions: 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,
            pending_questions: 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,
                    pending_questions: 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,
        pending_questions: None,
    }
}

// =============================================================================
// Content Viewing Tool Execution Functions
// =============================================================================

fn execute_view_body(current_body: &[BodyElement]) -> ToolExecutionResult {
    if current_body.is_empty() {
        return ToolExecutionResult {
            result: ToolResult {
                success: true,
                message: "Body is empty (no elements)".to_string(),
            },
            new_body: None,
            new_summary: None,
            parsed_data: Some(json!([])),
            version_request: None,
            pending_questions: None,
        };
    }

    let elements: Vec<serde_json::Value> = current_body
        .iter()
        .enumerate()
        .map(|(i, element)| {
            match element {
                BodyElement::Heading { level, text } => json!({
                    "index": i,
                    "type": "heading",
                    "level": level,
                    "text": text
                }),
                BodyElement::Paragraph { text } => json!({
                    "index": i,
                    "type": "paragraph",
                    "text": text
                }),
                BodyElement::Code { language, content } => json!({
                    "index": i,
                    "type": "code",
                    "language": language,
                    "content": content
                }),
                BodyElement::List { ordered, items } => json!({
                    "index": i,
                    "type": "list",
                    "ordered": ordered,
                    "items": items
                }),
                BodyElement::Chart { chart_type, title, data, config } => json!({
                    "index": i,
                    "type": "chart",
                    "chartType": format!("{:?}", chart_type).to_lowercase(),
                    "title": title,
                    "data": data,
                    "config": config
                }),
                BodyElement::Image { src, alt, caption } => json!({
                    "index": i,
                    "type": "image",
                    "src": src,
                    "alt": alt,
                    "caption": caption
                }),
                BodyElement::Markdown { content } => json!({
                    "index": i,
                    "type": "markdown",
                    "content": content
                }),
            }
        })
        .collect();

    ToolExecutionResult {
        result: ToolResult {
            success: true,
            message: format!("Body contains {} element(s)", current_body.len()),
        },
        new_body: None,
        new_summary: None,
        parsed_data: Some(json!(elements)),
        version_request: None,
        pending_questions: None,
    }
}

fn execute_read_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,
            pending_questions: 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,
            pending_questions: None,
        };
    }

    let element = &current_body[index];
    let element_data = match element {
        BodyElement::Heading { level, text } => json!({
            "index": index,
            "type": "heading",
            "level": level,
            "text": text
        }),
        BodyElement::Paragraph { text } => json!({
            "index": index,
            "type": "paragraph",
            "text": text
        }),
        BodyElement::Code { language, content } => json!({
            "index": index,
            "type": "code",
            "language": language,
            "content": content
        }),
        BodyElement::List { ordered, items } => json!({
            "index": index,
            "type": "list",
            "ordered": ordered,
            "items": items
        }),
        BodyElement::Chart { chart_type, title, data, config } => json!({
            "index": index,
            "type": "chart",
            "chartType": format!("{:?}", chart_type).to_lowercase(),
            "title": title,
            "data": data,
            "config": config
        }),
        BodyElement::Image { src, alt, caption } => json!({
            "index": index,
            "type": "image",
            "src": src,
            "alt": alt,
            "caption": caption
        }),
        BodyElement::Markdown { content } => json!({
            "index": index,
            "type": "markdown",
            "content": content
        }),
    };

    let type_str = match element {
        BodyElement::Heading { .. } => "heading",
        BodyElement::Paragraph { .. } => "paragraph",
        BodyElement::Code { .. } => "code",
        BodyElement::List { .. } => "list",
        BodyElement::Chart { .. } => "chart",
        BodyElement::Image { .. } => "image",
        BodyElement::Markdown { .. } => "markdown",
    };

    ToolExecutionResult {
        result: ToolResult {
            success: true,
            message: format!("Element {} is a {}", index, type_str),
        },
        new_body: None,
        new_summary: None,
        parsed_data: Some(element_data),
        version_request: None,
        pending_questions: None,
    }
}

fn execute_view_transcript(transcript: &[TranscriptEntry]) -> ToolExecutionResult {
    if transcript.is_empty() {
        return ToolExecutionResult {
            result: ToolResult {
                success: true,
                message: "Transcript is empty".to_string(),
            },
            new_body: None,
            new_summary: None,
            parsed_data: Some(json!([])),
            version_request: None,
            pending_questions: None,
        };
    }

    let entries: Vec<serde_json::Value> = transcript
        .iter()
        .enumerate()
        .map(|(i, entry)| {
            json!({
                "index": i,
                "speaker": entry.speaker,
                "text": entry.text,
                "start": entry.start,
                "end": entry.end
            })
        })
        .collect();

    // Calculate duration from timestamps
    let duration_info = if let (Some(first), Some(last)) = (transcript.first(), transcript.last()) {
        let duration_secs = last.end - first.start;
        let minutes = (duration_secs / 60.0).floor() as u32;
        let seconds = (duration_secs % 60.0).round() as u32;
        format!(" (duration: {}:{:02})", minutes, seconds)
    } else {
        String::new()
    };

    ToolExecutionResult {
        result: ToolResult {
            success: true,
            message: format!("Transcript has {} entries{}", transcript.len(), duration_info),
        },
        new_body: None,
        new_summary: None,
        parsed_data: Some(json!(entries)),
        version_request: None,
        pending_questions: 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),
        pending_questions: None,
    }
}

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,
            pending_questions: 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 }),
        pending_questions: None,
    }
}

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,
            pending_questions: 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,
        }),
        pending_questions: None,
    }
}

// =============================================================================
// Template Tool Execution Functions
// =============================================================================

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

    let template_list = match phase {
        Some(p) => templates::templates_for_phase(p),
        None => templates::all_templates(),
    };

    if template_list.is_empty() {
        return ToolExecutionResult {
            result: ToolResult {
                success: true,
                message: format!(
                    "No templates available for phase: {}",
                    phase.unwrap_or("(none)")
                ),
            },
            new_body: None,
            new_summary: None,
            parsed_data: Some(json!([])),
            version_request: None,
            pending_questions: None,
        };
    }

    // Convert templates to JSON (without the full body for display)
    let templates_json: Vec<serde_json::Value> = template_list
        .iter()
        .map(|t| {
            json!({
                "id": t.id,
                "name": t.name,
                "phase": t.phase,
                "description": t.description,
                "elementCount": t.suggested_body.len()
            })
        })
        .collect();

    let phase_msg = phase
        .map(|p| format!(" for '{}' phase", p))
        .unwrap_or_default();

    ToolExecutionResult {
        result: ToolResult {
            success: true,
            message: format!(
                "Found {} template(s){}. Use apply_template with a template_id to apply one.",
                templates_json.len(),
                phase_msg
            ),
        },
        new_body: None,
        new_summary: None,
        parsed_data: Some(json!(templates_json)),
        version_request: None,
        pending_questions: None,
    }
}

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

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

    // Find the template
    let all = templates::all_templates();
    let template = all.iter().find(|t| t.id == template_id);

    let Some(template) = template else {
        let available: Vec<String> = all.iter().map(|t| t.id.clone()).collect();
        return ToolExecutionResult {
            result: ToolResult {
                success: false,
                message: format!(
                    "Template '{}' not found. Available: {}",
                    template_id,
                    available.join(", ")
                ),
            },
            new_body: None,
            new_summary: None,
            parsed_data: None,
            version_request: None,
            pending_questions: None,
        };
    };

    ToolExecutionResult {
        result: ToolResult {
            success: true,
            message: format!(
                "Applied template '{}' ({}) with {} elements. You can now customize the content.",
                template.name,
                template.phase,
                template.suggested_body.len()
            ),
        },
        new_body: Some(template.suggested_body.clone()),
        new_summary: None,
        parsed_data: None,
        version_request: None,
        pending_questions: None,
    }
}

/// 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)
        }
    }
}