//! 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<Vec<Tool>> = 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<String>,
local_only: bool,
},
}
/// Result from executing a discussion tool
#[derive(Debug)]
pub struct DiscussToolExecutionResult {
pub success: bool,
pub message: String,
pub data: Option<serde_json::Value>,
/// Request for async operations (handled by discuss handler)
pub request: Option<DiscussToolRequest>,
/// Questions to ask the user (pauses conversation)
pub pending_questions: Option<Vec<super::tools::UserQuestion>>,
}
/// 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<String> = 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,
}
}