summaryrefslogblamecommitdiff
path: root/makima/src/llm/discuss_tools.rs
blob: 7330db3ea06c02a0cd96ede27ec3e49cc6c23fce (plain) (tree)

















































































































































































































                                                                                                                                                                       
//! 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,
    }
}