From 87044a747b47bd83249d61a45842c7f7b2eae56d Mon Sep 17 00:00:00 2001 From: soryu Date: Sun, 11 Jan 2026 05:52:14 +0000 Subject: Contract system --- makima/src/llm/tools.rs | 170 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) (limited to 'makima/src/llm/tools.rs') diff --git a/makima/src/llm/tools.rs b/makima/src/llm/tools.rs index 649633e..ae1dc5a 100644 --- a/makima/src/llm/tools.rs +++ b/makima/src/llm/tools.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use crate::db::models::{BodyElement, ChartType, TranscriptEntry}; +use crate::llm::templates; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Tool { @@ -411,6 +412,36 @@ pub static AVAILABLE_TOOLS: once_cell::sync::Lazy> = "required": ["target_version"] }), }, + // Template tools + Tool { + name: "suggest_templates".to_string(), + description: "Get suggested file templates based on a contract phase. Returns templates with predefined structures appropriate for research, specify, plan, execute, or review phases. Use this to help users start documents with proper structure.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "phase": { + "type": "string", + "enum": ["research", "specify", "plan", "execute", "review"], + "description": "The contract phase to get templates for. If not provided, returns all templates." + } + }, + "required": [] + }), + }, + Tool { + name: "apply_template".to_string(), + description: "Apply a template to the current file, replacing the body with the template structure. The template provides a starting structure that should be customized for the user's needs.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "template_id": { + "type": "string", + "description": "The template ID to apply (e.g., 'research-notes', 'requirements', 'architecture')" + } + }, + "required": ["template_id"] + }), + }, ] }); @@ -500,6 +531,9 @@ pub fn execute_tool_call( "list_versions" => execute_list_versions(), "read_version" => execute_read_version(call), "restore_version" => execute_restore_version(call), + // Template tools + "suggest_templates" => execute_suggest_templates(call), + "apply_template" => execute_apply_template(call), _ => ToolExecutionResult { result: ToolResult { success: false, @@ -1350,6 +1384,11 @@ fn execute_view_body(current_body: &[BodyElement]) -> ToolExecutionResult { "alt": alt, "caption": caption }), + BodyElement::Markdown { content } => json!({ + "index": i, + "type": "markdown", + "content": content + }), } }) .collect(); @@ -1439,6 +1478,11 @@ fn execute_read_element(call: &ToolCall, current_body: &[BodyElement]) -> ToolEx "alt": alt, "caption": caption }), + BodyElement::Markdown { content } => json!({ + "index": index, + "type": "markdown", + "content": content + }), }; let type_str = match element { @@ -1448,6 +1492,7 @@ fn execute_read_element(call: &ToolCall, current_body: &[BodyElement]) -> ToolEx BodyElement::List { .. } => "list", BodyElement::Chart { .. } => "chart", BodyElement::Image { .. } => "image", + BodyElement::Markdown { .. } => "markdown", }; ToolExecutionResult { @@ -1603,6 +1648,131 @@ fn execute_restore_version(call: &ToolCall) -> ToolExecutionResult { } } +// ============================================================================= +// Template Tool Execution Functions +// ============================================================================= + +fn execute_suggest_templates(call: &ToolCall) -> ToolExecutionResult { + let phase = call.arguments.get("phase").and_then(|v| v.as_str()); + + let template_list = match phase { + Some(p) => templates::templates_for_phase(p), + None => templates::all_templates(), + }; + + if template_list.is_empty() { + return ToolExecutionResult { + result: ToolResult { + success: true, + message: format!( + "No templates available for phase: {}", + phase.unwrap_or("(none)") + ), + }, + new_body: None, + new_summary: None, + parsed_data: Some(json!([])), + version_request: None, + pending_questions: None, + }; + } + + // Convert templates to JSON (without the full body for display) + let templates_json: Vec = template_list + .iter() + .map(|t| { + json!({ + "id": t.id, + "name": t.name, + "phase": t.phase, + "description": t.description, + "elementCount": t.suggested_body.len() + }) + }) + .collect(); + + let phase_msg = phase + .map(|p| format!(" for '{}' phase", p)) + .unwrap_or_default(); + + ToolExecutionResult { + result: ToolResult { + success: true, + message: format!( + "Found {} template(s){}. Use apply_template with a template_id to apply one.", + templates_json.len(), + phase_msg + ), + }, + new_body: None, + new_summary: None, + parsed_data: Some(json!(templates_json)), + version_request: None, + pending_questions: None, + } +} + +fn execute_apply_template(call: &ToolCall) -> ToolExecutionResult { + let template_id = call + .arguments + .get("template_id") + .and_then(|v| v.as_str()); + + let Some(template_id) = template_id else { + return ToolExecutionResult { + result: ToolResult { + success: false, + message: "Missing template_id parameter".to_string(), + }, + new_body: None, + new_summary: None, + parsed_data: None, + version_request: None, + pending_questions: None, + }; + }; + + // Find the template + let all = templates::all_templates(); + let template = all.iter().find(|t| t.id == template_id); + + let Some(template) = template else { + let available: Vec = all.iter().map(|t| t.id.clone()).collect(); + return ToolExecutionResult { + result: ToolResult { + success: false, + message: format!( + "Template '{}' not found. Available: {}", + template_id, + available.join(", ") + ), + }, + new_body: None, + new_summary: None, + parsed_data: None, + version_request: None, + pending_questions: None, + }; + }; + + ToolExecutionResult { + result: ToolResult { + success: true, + message: format!( + "Applied template '{}' ({}) with {} elements. You can now customize the content.", + template.name, + template.phase, + template.suggested_body.len() + ), + }, + new_body: Some(template.suggested_body.clone()), + new_summary: None, + parsed_data: None, + version_request: None, + pending_questions: None, + } +} + /// Convert serde_json::Value to jaq_interpret::Val fn json_to_jaq(value: &serde_json::Value) -> jaq_interpret::Val { match value { -- cgit v1.2.3