diff options
| author | soryu <soryu@soryu.co> | 2026-01-02 19:56:51 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-02 21:15:42 +0000 |
| commit | 062ae51396e88a8998bb30e78381275d77e7c90e (patch) | |
| tree | 79e40050a55250676c12c2ae5c4c6d8cbc1e53a4 /makima/src/llm/tools.rs | |
| parent | 2faba0388f93d8e4fb86219eba7883b331d501ff (diff) | |
| download | soryu-062ae51396e88a8998bb30e78381275d77e7c90e.tar.gz soryu-062ae51396e88a8998bb30e78381275d77e7c90e.zip | |
Add loop based LLM editing + view tools
Diffstat (limited to 'makima/src/llm/tools.rs')
| -rw-r--r-- | makima/src/llm/tools.rs | 234 |
1 files changed, 233 insertions, 1 deletions
diff --git a/makima/src/llm/tools.rs b/makima/src/llm/tools.rs index 35f321f..216b733 100644 --- a/makima/src/llm/tools.rs +++ b/makima/src/llm/tools.rs @@ -4,7 +4,7 @@ use jaq_interpret::FilterT; use serde::{Deserialize, Serialize}; use serde_json::json; -use crate::db::models::{BodyElement, ChartType}; +use crate::db::models::{BodyElement, ChartType, TranscriptEntry}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Tool { @@ -232,6 +232,39 @@ pub static AVAILABLE_TOOLS: once_cell::sync::Lazy<Vec<Tool>> = "required": ["input", "filter"] }), }, + // 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(), @@ -304,6 +337,7 @@ 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), @@ -316,6 +350,10 @@ pub fn execute_tool_call( "parse_csv" => execute_parse_csv(call), "clear_body" => execute_clear_body(), "jq" => execute_jq(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), @@ -897,6 +935,200 @@ fn execute_jq(call: &ToolCall) -> ToolExecutionResult { } // ============================================================================= +// 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, + }; + } + + 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::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 + }), + } + }) + .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, + } +} + +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, + }; + }; + + 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 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::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 + }), + }; + + let type_str = match element { + BodyElement::Heading { .. } => "heading", + BodyElement::Paragraph { .. } => "paragraph", + BodyElement::Chart { .. } => "chart", + BodyElement::Image { .. } => "image", + }; + + 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, + } +} + +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, + }; + } + + 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, + } +} + +// ============================================================================= // Version History Tool Execution Functions // ============================================================================= // These return version_request instead of performing the operation directly, |
