From 9ebc9724afcc0482a8e7cd2369c06208fedbcbd1 Mon Sep 17 00:00:00 2001 From: soryu Date: Tue, 3 Feb 2026 23:48:41 +0000 Subject: Add 'Discuss Contract' feature to listen page (#57) --- makima/src/llm/discuss_tools.rs | 210 ++++++++++++++++++++++++++++++++++++++++ makima/src/llm/mod.rs | 4 + 2 files changed, 214 insertions(+) create mode 100644 makima/src/llm/discuss_tools.rs (limited to 'makima/src/llm') diff --git a/makima/src/llm/discuss_tools.rs b/makima/src/llm/discuss_tools.rs new file mode 100644 index 0000000..7330db3 --- /dev/null +++ b/makima/src/llm/discuss_tools.rs @@ -0,0 +1,210 @@ +//! 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, + } +} diff --git a/makima/src/llm/mod.rs b/makima/src/llm/mod.rs index a3a3daf..4c84ced 100644 --- a/makima/src/llm/mod.rs +++ b/makima/src/llm/mod.rs @@ -2,6 +2,7 @@ pub mod claude; pub mod contract_tools; +pub mod discuss_tools; pub mod groq; pub mod markdown; pub mod mesh_tools; @@ -16,6 +17,9 @@ pub use contract_tools::{ parse_contract_tool_call, ChainedTaskDef, ContractToolExecutionResult, ContractToolRequest, CONTRACT_TOOLS, }; +pub use discuss_tools::{ + parse_discuss_tool_call, DiscussToolExecutionResult, DiscussToolRequest, DISCUSS_TOOLS, +}; pub use groq::GroqClient; pub use mesh_tools::{parse_mesh_tool_call, MeshToolExecutionResult, MeshToolRequest, MESH_TOOLS}; pub use phase_guidance::{ -- cgit v1.2.3