summaryrefslogtreecommitdiff
path: root/makima/src/llm/tools.rs
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/llm/tools.rs')
-rw-r--r--makima/src/llm/tools.rs1675
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 = &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,
- }
-}
-
-/// 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)
- }
- }
-}