summaryrefslogtreecommitdiff
path: root/makima/src/llm
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-02 19:56:51 +0000
committersoryu <soryu@soryu.co>2026-01-02 21:15:42 +0000
commit062ae51396e88a8998bb30e78381275d77e7c90e (patch)
tree79e40050a55250676c12c2ae5c4c6d8cbc1e53a4 /makima/src/llm
parent2faba0388f93d8e4fb86219eba7883b331d501ff (diff)
downloadsoryu-062ae51396e88a8998bb30e78381275d77e7c90e.tar.gz
soryu-062ae51396e88a8998bb30e78381275d77e7c90e.zip
Add loop based LLM editing + view tools
Diffstat (limited to 'makima/src/llm')
-rw-r--r--makima/src/llm/tools.rs234
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 = &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::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,