diff options
Diffstat (limited to 'makima/src/llm/tools.rs')
| -rw-r--r-- | makima/src/llm/tools.rs | 1675 |
1 files changed, 0 insertions, 1675 deletions
diff --git a/makima/src/llm/tools.rs b/makima/src/llm/tools.rs deleted file mode 100644 index c192398..0000000 --- a/makima/src/llm/tools.rs +++ /dev/null @@ -1,1675 +0,0 @@ -//! 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}; - -#[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. Questions can be single-select (user picks one option) or multi-select (user can pick multiple options). The question text supports markdown formatting. 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. Supports markdown formatting (bold, code, lists, etc.)" - }, - "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 (multi-select). If false or omitted, user selects exactly one option (single-select). Default: false" - }, - "allowCustom": { - "type": "boolean", - "description": "If true, user can provide a custom text 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"] - }), - }, - ] - }); - -/// 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), - _ => 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 = ¤t_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, - } -} - -/// 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) - } - } -} |
