//! Tool definitions for file editing via LLM.
use jaq_interpret::FilterT;
use serde::{Deserialize, Serialize};
use serde_json::json;
use crate::db::models::{BodyElement, ChartType};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tool {
pub name: String,
pub description: String,
pub parameters: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
pub name: String,
pub arguments: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct ToolResult {
pub success: bool,
pub message: String,
}
/// Available tools for file editing
pub static AVAILABLE_TOOLS: once_cell::sync::Lazy<Vec<Tool>> =
once_cell::sync::Lazy::new(|| {
vec![
Tool {
name: "add_heading".to_string(),
description: "Add a heading element to the file body".to_string(),
parameters: json!({
"type": "object",
"properties": {
"level": {
"type": "integer",
"description": "Heading level (1-6)",
"minimum": 1,
"maximum": 6
},
"text": {
"type": "string",
"description": "The heading text"
},
"position": {
"type": "integer",
"description": "Optional position to insert at (0-indexed). If not specified, appends to end."
}
},
"required": ["level", "text"]
}),
},
Tool {
name: "add_paragraph".to_string(),
description: "Add a paragraph element to the file body".to_string(),
parameters: json!({
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "The paragraph text"
},
"position": {
"type": "integer",
"description": "Optional position to insert at (0-indexed). If not specified, appends to end."
}
},
"required": ["text"]
}),
},
Tool {
name: "add_chart".to_string(),
description: "Add a chart visualization to the file body. Supports line, bar, pie, and area charts.".to_string(),
parameters: json!({
"type": "object",
"properties": {
"chart_type": {
"type": "string",
"enum": ["line", "bar", "pie", "area"],
"description": "Type of chart to create"
},
"title": {
"type": "string",
"description": "Optional chart title"
},
"data": {
"type": "array",
"description": "Array of data points. Each point should have a 'name' field and one or more numeric value fields.",
"items": {
"type": "object"
}
},
"config": {
"type": "object",
"description": "Optional chart configuration (colors, axes, etc.)"
},
"position": {
"type": "integer",
"description": "Optional position to insert at (0-indexed). If not specified, appends to end."
}
},
"required": ["chart_type", "data"]
}),
},
Tool {
name: "remove_element".to_string(),
description: "Remove an element from the file body by index".to_string(),
parameters: json!({
"type": "object",
"properties": {
"index": {
"type": "integer",
"description": "Index of element to remove (0-indexed)"
}
},
"required": ["index"]
}),
},
Tool {
name: "update_element".to_string(),
description: "Update an existing element in the file body. IMPORTANT: You must provide ALL required fields. For heading: type, level (1-6), text. For paragraph: type, text. For chart: type, chartType (line/bar/pie/area), data (array of objects).".to_string(),
parameters: json!({
"type": "object",
"properties": {
"index": {
"type": "integer",
"description": "Index of element to update (0-indexed)"
},
"element_type": {
"type": "string",
"enum": ["heading", "paragraph", "chart"],
"description": "Type of element"
},
"text": {
"type": "string",
"description": "Text content (required for heading and paragraph)"
},
"level": {
"type": "integer",
"description": "Heading level 1-6 (required for heading)"
},
"chartType": {
"type": "string",
"enum": ["line", "bar", "pie", "area"],
"description": "Chart type (required for chart)"
},
"data": {
"type": "array",
"description": "Chart data array (required for chart)",
"items": { "type": "object" }
},
"title": {
"type": "string",
"description": "Chart title (optional for chart)"
}
},
"required": ["index", "element_type"]
}),
},
Tool {
name: "reorder_elements".to_string(),
description: "Move an element from one position to another".to_string(),
parameters: json!({
"type": "object",
"properties": {
"from_index": {
"type": "integer",
"description": "Current index of the element"
},
"to_index": {
"type": "integer",
"description": "New index for the element"
}
},
"required": ["from_index", "to_index"]
}),
},
Tool {
name: "set_summary".to_string(),
description: "Set the file summary text".to_string(),
parameters: json!({
"type": "object",
"properties": {
"summary": {
"type": "string",
"description": "The summary text"
}
},
"required": ["summary"]
}),
},
Tool {
name: "parse_csv".to_string(),
description: "Parse CSV data into JSON format suitable for charts".to_string(),
parameters: json!({
"type": "object",
"properties": {
"csv": {
"type": "string",
"description": "CSV data string with header row"
}
},
"required": ["csv"]
}),
},
Tool {
name: "clear_body".to_string(),
description: "Clear all elements from the file body".to_string(),
parameters: json!({
"type": "object",
"properties": {}
}),
},
Tool {
name: "jq".to_string(),
description: "Transform JSON data using jq expressions. Useful for filtering, mapping, grouping, and aggregating data before creating charts.".to_string(),
parameters: json!({
"type": "object",
"properties": {
"input": {
"description": "The JSON data to transform (can be an array or object)"
},
"filter": {
"type": "string",
"description": "The jq filter expression. Examples: '.[] | select(.value > 10)', 'group_by(.category) | map({name: .[0].category, count: length})', '[.[] | {name: .label, value: .amount}]'"
}
},
"required": ["input", "filter"]
}),
},
// Version history tools
Tool {
name: "list_versions".to_string(),
description: "List all available versions of the current document. Returns version numbers, sources (user/llm/system), timestamps, and change descriptions.".to_string(),
parameters: json!({
"type": "object",
"properties": {},
"required": []
}),
},
Tool {
name: "read_version".to_string(),
description: "Read the content of a specific historical version of the document. This is read-only and does not modify the current document.".to_string(),
parameters: json!({
"type": "object",
"properties": {
"version": {
"type": "integer",
"description": "The version number to read"
}
},
"required": ["version"]
}),
},
Tool {
name: "restore_version".to_string(),
description: "Restore the document to a previous version. This creates a new version with the content from the target version. The current content will be preserved as a historical version.".to_string(),
parameters: json!({
"type": "object",
"properties": {
"target_version": {
"type": "integer",
"description": "The version number to restore to"
},
"reason": {
"type": "string",
"description": "Optional reason for the restore (will be recorded in change description)"
}
},
"required": ["target_version"]
}),
},
]
});
/// Request for version-related operations that require async database access
#[derive(Debug, Clone)]
pub enum VersionToolRequest {
/// List all versions of the current file
ListVersions,
/// Read a specific version
ReadVersion { version: i32 },
/// Restore to a specific version
RestoreVersion { target_version: i32, reason: Option<String> },
}
/// Result of executing a tool call with modified file state
#[derive(Debug)]
pub struct ToolExecutionResult {
pub result: ToolResult,
pub new_body: Option<Vec<BodyElement>>,
pub new_summary: Option<String>,
pub parsed_data: Option<serde_json::Value>,
/// Request for async version operations (handled by chat handler)
pub version_request: Option<VersionToolRequest>,
}
/// Execute a tool call and return the result along with any state changes
pub fn execute_tool_call(
call: &ToolCall,
current_body: &[BodyElement],
current_summary: Option<&str>,
) -> ToolExecutionResult {
match call.name.as_str() {
"add_heading" => execute_add_heading(call, current_body),
"add_paragraph" => execute_add_paragraph(call, current_body),
"add_chart" => execute_add_chart(call, current_body),
"remove_element" => execute_remove_element(call, current_body),
"update_element" => execute_update_element(call, current_body),
"reorder_elements" => execute_reorder_elements(call, current_body),
"set_summary" => execute_set_summary(call, current_summary),
"parse_csv" => execute_parse_csv(call),
"clear_body" => execute_clear_body(),
"jq" => execute_jq(call),
// Version history tools - return request for async handling
"list_versions" => execute_list_versions(),
"read_version" => execute_read_version(call),
"restore_version" => execute_restore_version(call),
_ => ToolExecutionResult {
result: ToolResult {
success: false,
message: format!("Unknown tool: {}", call.name),
},
new_body: None,
new_summary: None,
parsed_data: None,
version_request: None,
},
}
}
fn execute_add_heading(call: &ToolCall, current_body: &[BodyElement]) -> ToolExecutionResult {
let level = call.arguments.get("level").and_then(|v| v.as_u64()).unwrap_or(1) as u8;
let text = call
.arguments
.get("text")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let position = call.arguments.get("position").and_then(|v| v.as_u64());
let element = BodyElement::Heading { level, text: text.clone() };
let mut new_body = current_body.to_vec();
if let Some(pos) = position {
let pos = pos as usize;
if pos <= new_body.len() {
new_body.insert(pos, element);
} else {
new_body.push(element);
}
} else {
new_body.push(element);
}
ToolExecutionResult {
result: ToolResult {
success: true,
message: format!("Added heading: {}", text),
},
new_body: Some(new_body),
new_summary: None,
parsed_data: None,
version_request: None,
}
}
fn execute_add_paragraph(call: &ToolCall, current_body: &[BodyElement]) -> ToolExecutionResult {
let text = call
.arguments
.get("text")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let position = call.arguments.get("position").and_then(|v| v.as_u64());
let element = BodyElement::Paragraph { text: text.clone() };
let mut new_body = current_body.to_vec();
if let Some(pos) = position {
let pos = pos as usize;
if pos <= new_body.len() {
new_body.insert(pos, element);
} else {
new_body.push(element);
}
} else {
new_body.push(element);
}
let preview = if text.len() > 50 {
format!("{}...", &text[..50])
} else {
text
};
ToolExecutionResult {
result: ToolResult {
success: true,
message: format!("Added paragraph: {}", preview),
},
new_body: Some(new_body),
new_summary: None,
parsed_data: None,
version_request: None,
}
}
fn execute_add_chart(call: &ToolCall, current_body: &[BodyElement]) -> ToolExecutionResult {
let chart_type_str = call
.arguments
.get("chart_type")
.and_then(|v| v.as_str())
.unwrap_or("bar");
let chart_type = match chart_type_str {
"line" => ChartType::Line,
"bar" => ChartType::Bar,
"pie" => ChartType::Pie,
"area" => ChartType::Area,
_ => ChartType::Bar,
};
let title = call
.arguments
.get("title")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let data = call
.arguments
.get("data")
.cloned()
.unwrap_or(json!([]));
let config = call.arguments.get("config").cloned();
let position = call.arguments.get("position").and_then(|v| v.as_u64());
let element = BodyElement::Chart {
chart_type,
title: title.clone(),
data,
config,
};
let mut new_body = current_body.to_vec();
if let Some(pos) = position {
let pos = pos as usize;
if pos <= new_body.len() {
new_body.insert(pos, element);
} else {
new_body.push(element);
}
} else {
new_body.push(element);
}
ToolExecutionResult {
result: ToolResult {
success: true,
message: format!(
"Added {} chart{}",
chart_type_str,
title.map(|t| format!(": {}", t)).unwrap_or_default()
),
},
new_body: Some(new_body),
new_summary: None,
parsed_data: None,
version_request: None,
}
}
fn execute_remove_element(call: &ToolCall, current_body: &[BodyElement]) -> ToolExecutionResult {
let index = call.arguments.get("index").and_then(|v| v.as_u64());
let Some(index) = index else {
return ToolExecutionResult {
result: ToolResult {
success: false,
message: "Missing index parameter".to_string(),
},
new_body: None,
new_summary: None,
parsed_data: None,
version_request: None,
};
};
let index = index as usize;
if index >= current_body.len() {
return ToolExecutionResult {
result: ToolResult {
success: false,
message: format!("Index {} out of bounds (body has {} elements)", index, current_body.len()),
},
new_body: None,
new_summary: None,
parsed_data: None,
version_request: None,
};
}
let mut new_body = current_body.to_vec();
new_body.remove(index);
ToolExecutionResult {
result: ToolResult {
success: true,
message: format!("Removed element at index {}", index),
},
new_body: Some(new_body),
new_summary: None,
parsed_data: None,
version_request: None,
}
}
fn execute_update_element(call: &ToolCall, current_body: &[BodyElement]) -> ToolExecutionResult {
let index = call.arguments.get("index").and_then(|v| v.as_u64());
let element_type = call.arguments.get("element_type").and_then(|v| v.as_str());
let Some(index) = index else {
return ToolExecutionResult {
result: ToolResult {
success: false,
message: "Missing index parameter".to_string(),
},
new_body: None,
new_summary: None,
parsed_data: None,
version_request: None,
};
};
let Some(element_type) = element_type else {
return ToolExecutionResult {
result: ToolResult {
success: false,
message: "Missing element_type parameter".to_string(),
},
new_body: None,
new_summary: None,
parsed_data: None,
version_request: None,
};
};
let index = index as usize;
if index >= current_body.len() {
return ToolExecutionResult {
result: ToolResult {
success: false,
message: format!("Index {} out of bounds (body has {} elements)", index, current_body.len()),
},
new_body: None,
new_summary: None,
parsed_data: None,
version_request: None,
};
}
// Build the element based on type
let new_element = match element_type {
"heading" => {
let level = call.arguments.get("level").and_then(|v| v.as_u64()).unwrap_or(1) as u8;
let text = call.arguments.get("text").and_then(|v| v.as_str()).unwrap_or("").to_string();
BodyElement::Heading { level, text }
}
"paragraph" => {
let text = call.arguments.get("text").and_then(|v| v.as_str()).unwrap_or("").to_string();
BodyElement::Paragraph { text }
}
"chart" => {
let chart_type_str = call.arguments.get("chartType").and_then(|v| v.as_str()).unwrap_or("bar");
let chart_type = match chart_type_str {
"line" => ChartType::Line,
"bar" => ChartType::Bar,
"pie" => ChartType::Pie,
"area" => ChartType::Area,
_ => ChartType::Bar,
};
let title = call.arguments.get("title").and_then(|v| v.as_str()).map(|s| s.to_string());
let data = call.arguments.get("data").cloned().unwrap_or(json!([]));
let config = call.arguments.get("config").cloned();
BodyElement::Chart { chart_type, title, data, config }
}
_ => {
return ToolExecutionResult {
result: ToolResult {
success: false,
message: format!("Unknown element_type: {}. Must be heading, paragraph, or chart.", element_type),
},
new_body: None,
new_summary: None,
parsed_data: None,
version_request: None,
};
}
};
let mut new_body = current_body.to_vec();
new_body[index] = new_element;
ToolExecutionResult {
result: ToolResult {
success: true,
message: format!("Updated element at index {} to {}", index, element_type),
},
new_body: Some(new_body),
new_summary: None,
parsed_data: None,
version_request: None,
}
}
fn execute_reorder_elements(call: &ToolCall, current_body: &[BodyElement]) -> ToolExecutionResult {
let from_index = call.arguments.get("from_index").and_then(|v| v.as_u64());
let to_index = call.arguments.get("to_index").and_then(|v| v.as_u64());
let (Some(from), Some(to)) = (from_index, to_index) else {
return ToolExecutionResult {
result: ToolResult {
success: false,
message: "Missing from_index or to_index parameter".to_string(),
},
new_body: None,
new_summary: None,
parsed_data: None,
version_request: None,
};
};
let from = from as usize;
let to = to as usize;
if from >= current_body.len() || to >= current_body.len() {
return ToolExecutionResult {
result: ToolResult {
success: false,
message: format!(
"Index out of bounds: from={}, to={}, body has {} elements",
from, to, current_body.len()
),
},
new_body: None,
new_summary: None,
parsed_data: None,
version_request: None,
};
}
let mut new_body = current_body.to_vec();
let element = new_body.remove(from);
new_body.insert(to, element);
ToolExecutionResult {
result: ToolResult {
success: true,
message: format!("Moved element from index {} to {}", from, to),
},
new_body: Some(new_body),
new_summary: None,
parsed_data: None,
version_request: None,
}
}
fn execute_set_summary(call: &ToolCall, _current_summary: Option<&str>) -> ToolExecutionResult {
let summary = call
.arguments
.get("summary")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
ToolExecutionResult {
result: ToolResult {
success: true,
message: "Summary updated".to_string(),
},
new_body: None,
new_summary: Some(summary),
parsed_data: None,
version_request: None,
}
}
fn execute_parse_csv(call: &ToolCall) -> ToolExecutionResult {
let csv = call
.arguments
.get("csv")
.and_then(|v| v.as_str())
.unwrap_or("");
let lines: Vec<&str> = csv.lines().collect();
if lines.is_empty() {
return ToolExecutionResult {
result: ToolResult {
success: false,
message: "Empty CSV data".to_string(),
},
new_body: None,
new_summary: None,
parsed_data: None,
version_request: None,
};
}
let headers: Vec<&str> = lines[0].split(',').map(|s| s.trim()).collect();
let mut data: Vec<serde_json::Value> = Vec::new();
for line in lines.iter().skip(1) {
if line.trim().is_empty() {
continue;
}
let values: Vec<&str> = line.split(',').map(|s| s.trim()).collect();
let mut row = serde_json::Map::new();
for (i, header) in headers.iter().enumerate() {
if let Some(value) = values.get(i) {
// Try to parse as number, otherwise use string
if let Ok(num) = value.parse::<f64>() {
row.insert(header.to_string(), json!(num));
} else {
row.insert(header.to_string(), json!(value));
}
}
}
data.push(serde_json::Value::Object(row));
}
ToolExecutionResult {
result: ToolResult {
success: true,
message: format!("Parsed {} rows from CSV", data.len()),
},
new_body: None,
new_summary: None,
parsed_data: Some(json!(data)),
version_request: None,
}
}
fn execute_clear_body() -> ToolExecutionResult {
ToolExecutionResult {
result: ToolResult {
success: true,
message: "Cleared all body elements".to_string(),
},
new_body: Some(vec![]),
new_summary: None,
parsed_data: None,
version_request: None,
}
}
fn execute_jq(call: &ToolCall) -> ToolExecutionResult {
let input = match call.arguments.get("input") {
Some(v) => v.clone(),
None => {
return ToolExecutionResult {
result: ToolResult {
success: false,
message: "Missing input parameter".to_string(),
},
new_body: None,
new_summary: None,
parsed_data: None,
version_request: None,
};
}
};
let filter = match call.arguments.get("filter").and_then(|v| v.as_str()) {
Some(f) => f,
None => {
return ToolExecutionResult {
result: ToolResult {
success: false,
message: "Missing filter parameter".to_string(),
},
new_body: None,
new_summary: None,
parsed_data: None,
version_request: None,
};
}
};
// Parse the jq filter
let mut defs = jaq_interpret::ParseCtx::new(Vec::new());
defs.insert_natives(jaq_core::core());
defs.insert_defs(jaq_std::std());
let (parsed_filter, errs) = jaq_parse::parse(filter, jaq_parse::main());
if !errs.is_empty() {
return ToolExecutionResult {
result: ToolResult {
success: false,
message: format!("Invalid jq filter: {:?}", errs),
},
new_body: None,
new_summary: None,
parsed_data: None,
version_request: None,
};
}
let Some(parsed_filter) = parsed_filter else {
return ToolExecutionResult {
result: ToolResult {
success: false,
message: "Failed to parse jq filter".to_string(),
},
new_body: None,
new_summary: None,
parsed_data: None,
version_request: None,
};
};
// Compile the filter
let compiled = defs.compile(parsed_filter);
if !defs.errs.is_empty() {
return ToolExecutionResult {
result: ToolResult {
success: false,
message: format!("Failed to compile jq filter ({} errors)", defs.errs.len()),
},
new_body: None,
new_summary: None,
parsed_data: None,
version_request: None,
};
}
// Convert serde_json::Value to jaq Value
let jaq_input = json_to_jaq(&input);
// Execute the filter
let inputs = jaq_interpret::RcIter::new(std::iter::empty());
let mut results: Vec<serde_json::Value> = Vec::new();
for output in compiled.run((jaq_interpret::Ctx::new([], &inputs), jaq_input)) {
match output {
Ok(val) => {
results.push(jaq_to_json(&val));
}
Err(e) => {
return ToolExecutionResult {
result: ToolResult {
success: false,
message: format!("jq execution error: {:?}", e),
},
new_body: None,
new_summary: None,
parsed_data: None,
version_request: None,
};
}
}
}
// Return single value or array based on results
let output = if results.len() == 1 {
results.into_iter().next().unwrap()
} else {
json!(results)
};
let preview = {
let s = output.to_string();
if s.len() > 100 {
format!("{}...", &s[..100])
} else {
s
}
};
ToolExecutionResult {
result: ToolResult {
success: true,
message: format!("jq transform complete: {}", preview),
},
new_body: None,
new_summary: None,
parsed_data: Some(output),
version_request: None,
}
}
// =============================================================================
// Version History Tool Execution Functions
// =============================================================================
// These return version_request instead of performing the operation directly,
// because they require async database access which is handled in the chat handler.
fn execute_list_versions() -> ToolExecutionResult {
ToolExecutionResult {
result: ToolResult {
success: true,
message: "Listing versions...".to_string(),
},
new_body: None,
new_summary: None,
parsed_data: None,
version_request: Some(VersionToolRequest::ListVersions),
}
}
fn execute_read_version(call: &ToolCall) -> ToolExecutionResult {
let version = call.arguments.get("version").and_then(|v| v.as_i64());
let Some(version) = version else {
return ToolExecutionResult {
result: ToolResult {
success: false,
message: "Missing version parameter".to_string(),
},
new_body: None,
new_summary: None,
parsed_data: None,
version_request: None,
};
};
ToolExecutionResult {
result: ToolResult {
success: true,
message: format!("Reading version {}...", version),
},
new_body: None,
new_summary: None,
parsed_data: None,
version_request: Some(VersionToolRequest::ReadVersion { version: version as i32 }),
}
}
fn execute_restore_version(call: &ToolCall) -> ToolExecutionResult {
let target_version = call.arguments.get("target_version").and_then(|v| v.as_i64());
let reason = call
.arguments
.get("reason")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let Some(target_version) = target_version else {
return ToolExecutionResult {
result: ToolResult {
success: false,
message: "Missing target_version parameter".to_string(),
},
new_body: None,
new_summary: None,
parsed_data: None,
version_request: None,
};
};
ToolExecutionResult {
result: ToolResult {
success: true,
message: format!("Restoring to version {}...", target_version),
},
new_body: None,
new_summary: None,
parsed_data: None,
version_request: Some(VersionToolRequest::RestoreVersion {
target_version: target_version as i32,
reason,
}),
}
}
/// Convert serde_json::Value to jaq_interpret::Val
fn json_to_jaq(value: &serde_json::Value) -> jaq_interpret::Val {
match value {
serde_json::Value::Null => jaq_interpret::Val::Null,
serde_json::Value::Bool(b) => jaq_interpret::Val::Bool(*b),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
jaq_interpret::Val::Int(i as isize)
} else if let Some(f) = n.as_f64() {
jaq_interpret::Val::Float(f)
} else {
jaq_interpret::Val::Null
}
}
serde_json::Value::String(s) => jaq_interpret::Val::Str(s.clone().into()),
serde_json::Value::Array(arr) => {
jaq_interpret::Val::Arr(std::rc::Rc::new(arr.iter().map(json_to_jaq).collect()))
}
serde_json::Value::Object(obj) => {
let mut map: indexmap::IndexMap<std::rc::Rc<String>, jaq_interpret::Val, ahash::RandomState> =
indexmap::IndexMap::with_hasher(ahash::RandomState::new());
for (k, v) in obj {
map.insert(std::rc::Rc::new(k.clone()), json_to_jaq(v));
}
jaq_interpret::Val::Obj(std::rc::Rc::new(map))
}
}
}
/// Convert jaq_interpret::Val to serde_json::Value
fn jaq_to_json(value: &jaq_interpret::Val) -> serde_json::Value {
match value {
jaq_interpret::Val::Null => serde_json::Value::Null,
jaq_interpret::Val::Bool(b) => json!(*b),
jaq_interpret::Val::Int(i) => json!(*i),
jaq_interpret::Val::Float(f) => json!(*f),
jaq_interpret::Val::Num(n) => {
// Try to parse the number string
if let Ok(i) = n.parse::<i64>() {
json!(i)
} else if let Ok(f) = n.parse::<f64>() {
json!(f)
} else {
json!(n.as_ref())
}
}
jaq_interpret::Val::Str(s) => json!(s.as_ref()),
jaq_interpret::Val::Arr(arr) => {
json!(arr.iter().map(jaq_to_json).collect::<Vec<_>>())
}
jaq_interpret::Val::Obj(obj) => {
let mut map = serde_json::Map::new();
for (k, v) in obj.iter() {
map.insert((**k).clone(), jaq_to_json(v));
}
serde_json::Value::Object(map)
}
}
}