//! Tool definitions for contract discussion via LLM. //! //! These tools allow Makima to help users define and create contracts //! through natural conversation. use serde_json::json; use super::tools::Tool; /// Available tools for contract discussion pub static DISCUSS_TOOLS: once_cell::sync::Lazy> = once_cell::sync::Lazy::new(|| { vec![ Tool { name: "create_contract".to_string(), description: "Create a new contract based on the discussion. Only call this when the user has confirmed they're ready to create the contract.".to_string(), parameters: json!({ "type": "object", "properties": { "name": { "type": "string", "description": "Name for the contract" }, "description": { "type": "string", "description": "Detailed description of what the contract is for" }, "contract_type": { "type": "string", "enum": ["simple", "specification", "execute"], "description": "Type of contract workflow" }, "repository_url": { "type": "string", "description": "Optional repository URL if discussed" }, "local_only": { "type": "boolean", "description": "If true, tasks won't auto-push or create PRs" } }, "required": ["name", "description", "contract_type"] }), }, Tool { name: "ask_clarification".to_string(), description: "Ask the user a clarifying question with multiple choice options.".to_string(), parameters: json!({ "type": "object", "properties": { "question": { "type": "string", "description": "The question to ask" }, "options": { "type": "array", "items": { "type": "string" }, "description": "Multiple choice options" }, "allow_custom": { "type": "boolean", "description": "Allow user to provide a custom answer" } }, "required": ["question", "options"] }), }, ] }); /// Request for discussion tool operations that require async database access #[derive(Debug, Clone)] pub enum DiscussToolRequest { /// Create a new contract CreateContract { name: String, description: String, contract_type: String, repository_url: Option, local_only: bool, }, } /// Result from executing a discussion tool #[derive(Debug)] pub struct DiscussToolExecutionResult { pub success: bool, pub message: String, pub data: Option, /// Request for async operations (handled by discuss handler) pub request: Option, /// Questions to ask the user (pauses conversation) pub pending_questions: Option>, } /// Parse and validate a discussion tool call, returning a DiscussToolRequest for async handling pub fn parse_discuss_tool_call(call: &super::tools::ToolCall) -> DiscussToolExecutionResult { match call.name.as_str() { "create_contract" => parse_create_contract(call), "ask_clarification" => parse_ask_clarification(call), _ => DiscussToolExecutionResult { success: false, message: format!("Unknown discussion tool: {}", call.name), data: None, request: None, pending_questions: None, }, } } fn parse_create_contract(call: &super::tools::ToolCall) -> DiscussToolExecutionResult { let name = call.arguments.get("name").and_then(|v| v.as_str()); let description = call.arguments.get("description").and_then(|v| v.as_str()); let contract_type = call.arguments.get("contract_type").and_then(|v| v.as_str()); let Some(name) = name else { return error_result("Missing required parameter: name"); }; let Some(description) = description else { return error_result("Missing required parameter: description"); }; let Some(contract_type) = contract_type else { return error_result("Missing required parameter: contract_type"); }; let valid_types = ["simple", "specification", "execute"]; if !valid_types.contains(&contract_type) { return error_result("Invalid contract_type. Must be one of: simple, specification, execute"); } let repository_url = call .arguments .get("repository_url") .and_then(|v| v.as_str()) .map(|s| s.to_string()); let local_only = call .arguments .get("local_only") .and_then(|v| v.as_bool()) .unwrap_or(false); DiscussToolExecutionResult { success: true, message: format!("Creating contract '{}'...", name), data: None, request: Some(DiscussToolRequest::CreateContract { name: name.to_string(), description: description.to_string(), contract_type: contract_type.to_string(), repository_url, local_only, }), pending_questions: None, } } fn parse_ask_clarification(call: &super::tools::ToolCall) -> DiscussToolExecutionResult { let question = call.arguments.get("question").and_then(|v| v.as_str()); let options = call.arguments.get("options").and_then(|v| v.as_array()); let Some(question) = question else { return error_result("Missing required parameter: question"); }; let Some(options) = options else { return error_result("Missing required parameter: options"); }; let options: Vec = options .iter() .filter_map(|o| o.as_str()) .map(|s| s.to_string()) .collect(); if options.is_empty() { return error_result("Options array cannot be empty"); } let allow_custom = call .arguments .get("allow_custom") .and_then(|v| v.as_bool()) .unwrap_or(true); // Create a UserQuestion for the ask_clarification tool let user_question = super::tools::UserQuestion { id: "clarification".to_string(), question: question.to_string(), options, allow_multiple: false, allow_custom, }; DiscussToolExecutionResult { success: true, message: format!("Asking clarification: {}", question), data: None, request: None, pending_questions: Some(vec![user_question]), } } fn error_result(message: &str) -> DiscussToolExecutionResult { DiscussToolExecutionResult { success: false, message: message.to_string(), data: None, request: None, pending_questions: None, } }