diff options
Diffstat (limited to 'makima/src')
| -rw-r--r-- | makima/src/llm/discuss_tools.rs | 210 | ||||
| -rw-r--r-- | makima/src/llm/mod.rs | 4 | ||||
| -rw-r--r-- | makima/src/server/handlers/contract_discuss.rs | 592 | ||||
| -rw-r--r-- | makima/src/server/handlers/mod.rs | 1 | ||||
| -rw-r--r-- | makima/src/server/mod.rs | 3 | ||||
| -rw-r--r-- | makima/src/server/openapi.rs | 10 |
6 files changed, 818 insertions, 2 deletions
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<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, + } +} 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::{ diff --git a/makima/src/server/handlers/contract_discuss.rs b/makima/src/server/handlers/contract_discuss.rs new file mode 100644 index 0000000..1f98f53 --- /dev/null +++ b/makima/src/server/handlers/contract_discuss.rs @@ -0,0 +1,592 @@ +//! Discussion endpoint for LLM-powered contract creation. +//! +//! This handler provides an ephemeral conversation with Makima to help users +//! define and create contracts through natural dialogue. + +use axum::{ + extract::State, + http::StatusCode, + response::IntoResponse, + Json, +}; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use utoipa::ToSchema; +use uuid::Uuid; + +use crate::db::{models::CreateContractRequest, repository}; +use crate::llm::{ + claude::{self, ClaudeClient, ClaudeError, ClaudeModel}, + groq::{GroqClient, GroqError, Message, ToolCallResponse}, + discuss_tools::{parse_discuss_tool_call, DiscussToolRequest, DISCUSS_TOOLS}, + LlmModel, ToolCall, ToolResult, UserQuestion, +}; +use crate::server::auth::Authenticated; +use crate::server::state::SharedState; + +/// Maximum number of tool-calling rounds to prevent infinite loops +const MAX_TOOL_ROUNDS: usize = 10; + +/// System prompt for Makima character in contract discussions +const DISCUSS_SYSTEM_PROMPT: &str = r#" +You are Makima, an AI assistant on the makima.jp platform. You help users define and create contracts for their projects through natural conversation. + +## Your Personality +- Professional yet personable +- Focused on understanding the user's actual needs +- Ask clarifying questions when requirements are vague +- Guide the conversation toward actionable outcomes +- Comfortable making recommendations based on experience + +## Your Goal +Help the user flesh out their project idea into a well-defined contract. A contract on makima.jp includes: +- A clear name and description +- The right contract type (simple, specification, or execute) +- Understanding of the scope and requirements + +## Contract Types +- **simple**: Quick tasks with minimal planning (plan -> execute phases only) +- **specification**: Full lifecycle projects (research -> specify -> plan -> execute -> review) +- **execute**: Direct implementation when requirements are already clear (execute phase only) + +## Guidelines +1. **Start by understanding**: Ask about what they want to build +2. **Clarify scope**: Is this a quick fix, a new feature, or a full project? +3. **Gather requirements**: What are the must-haves vs nice-to-haves? +4. **Identify context**: Is there existing code? Which repository? +5. **Recommend type**: Suggest the appropriate contract type +6. **Confirm and create**: When the user is satisfied, create the contract + +## When to Create the Contract +Create the contract when: +- You have a clear understanding of what the user wants +- The user has confirmed they're ready to proceed +- You've gathered enough information for a meaningful contract + +Do NOT create the contract if: +- The user is still exploring ideas +- Key information is missing +- The user hasn't indicated readiness + +{transcript_context} +"#; + +/// Chat message in history +#[derive(Debug, Clone, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ChatMessage { + /// Role: "user" or "assistant" + pub role: String, + /// Message content + pub content: String, +} + +/// Request to discuss a potential contract +#[derive(Debug, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DiscussContractRequest { + /// The user's message + pub message: String, + /// Optional model selection (default: claude-sonnet) + #[serde(default)] + pub model: Option<String>, + /// Conversation history for context continuity + #[serde(default)] + pub history: Option<Vec<ChatMessage>>, + /// Optional transcript context from current session + #[serde(default)] + pub transcript_context: Option<String>, +} + +/// Response from the discussion endpoint +#[derive(Debug, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DiscussContractResponse { + /// Makima's response message + pub response: String, + /// Tool calls that were executed (e.g., create_contract) + pub tool_calls: Vec<ToolCallInfo>, + /// If a contract was created, its details + #[serde(skip_serializing_if = "Option::is_none")] + pub created_contract: Option<CreatedContractInfo>, + /// Pending questions (if LLM needs clarification) + #[serde(skip_serializing_if = "Option::is_none")] + pub pending_questions: Option<Vec<UserQuestion>>, +} + +/// Information about a tool call that was executed +#[derive(Debug, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ToolCallInfo { + pub name: String, + pub result: ToolResult, +} + +/// Information about a created contract +#[derive(Debug, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct CreatedContractInfo { + pub id: String, + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option<String>, + pub contract_type: String, + pub initial_phase: String, +} + +/// Enum to hold LLM clients +enum LlmClient { + Groq(GroqClient), + Claude(ClaudeClient), +} + +/// Unified result from LLM call +struct LlmResult { + content: Option<String>, + tool_calls: Vec<ToolCall>, + raw_tool_calls: Vec<ToolCallResponse>, + finish_reason: String, +} + +/// Discuss a potential contract with Makima +#[utoipa::path( + post, + path = "/api/v1/contracts/discuss", + request_body = DiscussContractRequest, + responses( + (status = 200, description = "Discussion completed successfully", body = DiscussContractResponse), + (status = 401, description = "Unauthorized"), + (status = 500, description = "Internal server error") + ), + security( + ("bearer_auth" = []), + ("api_key" = []) + ), + tag = "Contracts" +)] +pub async fn discuss_contract_handler( + State(state): State<SharedState>, + Authenticated(auth): Authenticated, + Json(request): Json<DiscussContractRequest>, +) -> impl IntoResponse { + // Check if database is configured + let Some(ref pool) = state.db_pool else { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(json!({ "error": "Database not configured" })), + ) + .into_response(); + }; + + // Parse model selection (default to Claude Sonnet) + let model = request + .model + .as_ref() + .and_then(|m| LlmModel::from_str(m)) + .unwrap_or(LlmModel::ClaudeSonnet); + + tracing::info!("Contract discussion using LLM model: {:?}", model); + + // Initialize the appropriate LLM client + let llm_client = match model { + LlmModel::ClaudeSonnet => match ClaudeClient::from_env(ClaudeModel::Sonnet) { + Ok(client) => LlmClient::Claude(client), + Err(ClaudeError::MissingApiKey) => { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(json!({ "error": "ANTHROPIC_API_KEY not configured" })), + ) + .into_response(); + } + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({ "error": format!("Claude client error: {}", e) })), + ) + .into_response(); + } + }, + LlmModel::ClaudeOpus => match ClaudeClient::from_env(ClaudeModel::Opus) { + Ok(client) => LlmClient::Claude(client), + Err(ClaudeError::MissingApiKey) => { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(json!({ "error": "ANTHROPIC_API_KEY not configured" })), + ) + .into_response(); + } + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({ "error": format!("Claude client error: {}", e) })), + ) + .into_response(); + } + }, + LlmModel::GroqKimi => match GroqClient::from_env() { + Ok(client) => LlmClient::Groq(client), + Err(GroqError::MissingApiKey) => { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(json!({ "error": "GROQ_API_KEY not configured" })), + ) + .into_response(); + } + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({ "error": format!("Groq client error: {}", e) })), + ) + .into_response(); + } + }, + }; + + // Build system prompt with optional transcript context + let transcript_section = match &request.transcript_context { + Some(ctx) => format!( + "\n## Current Session Context\nThe user has been recording a session. Here's the transcript:\n\n{}\n", + ctx + ), + None => String::new(), + }; + + let system_prompt = DISCUSS_SYSTEM_PROMPT.replace("{transcript_context}", &transcript_section); + + // Run the discussion agentic loop + run_discuss_agentic_loop( + pool, + &llm_client, + system_prompt, + &request, + auth.owner_id, + ) + .await +} + +/// Run the agentic loop for contract discussion +async fn run_discuss_agentic_loop( + pool: &sqlx::PgPool, + llm_client: &LlmClient, + system_prompt: String, + request: &DiscussContractRequest, + owner_id: Uuid, +) -> axum::response::Response { + // Build initial messages + let mut messages = vec![Message { + role: "system".to_string(), + content: Some(system_prompt), + tool_calls: None, + tool_call_id: None, + }]; + + // Add conversation history if provided + if let Some(history) = &request.history { + for msg in history { + messages.push(Message { + role: msg.role.clone(), + content: Some(msg.content.clone()), + tool_calls: None, + tool_call_id: None, + }); + } + } + + // Add current user message + messages.push(Message { + role: "user".to_string(), + content: Some(request.message.clone()), + tool_calls: None, + tool_call_id: None, + }); + + // State for tracking + let mut all_tool_call_infos: Vec<ToolCallInfo> = Vec::new(); + let mut final_response: Option<String> = None; + let mut created_contract: Option<CreatedContractInfo> = None; + let mut pending_questions: Option<Vec<UserQuestion>> = None; + + // Multi-turn agentic tool calling loop + for round in 0..MAX_TOOL_ROUNDS { + tracing::info!( + round = round, + total_tool_calls = all_tool_call_infos.len(), + "Contract discussion loop iteration" + ); + + // Call the appropriate LLM API + let result = match llm_client { + LlmClient::Groq(groq) => { + match groq.chat_with_tools(messages.clone(), &DISCUSS_TOOLS).await { + Ok(r) => LlmResult { + content: r.content, + tool_calls: r.tool_calls, + raw_tool_calls: r.raw_tool_calls, + finish_reason: r.finish_reason, + }, + Err(e) => { + tracing::error!("Groq API error: {}", e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({ "error": format!("LLM API error: {}", e) })), + ) + .into_response(); + } + } + } + LlmClient::Claude(claude_client) => { + let claude_messages = claude::groq_messages_to_claude(&messages); + match claude_client + .chat_with_tools(claude_messages, &DISCUSS_TOOLS) + .await + { + Ok(r) => { + let raw_tool_calls: Vec<ToolCallResponse> = r + .tool_calls + .iter() + .map(|tc| ToolCallResponse { + id: tc.id.clone(), + call_type: "function".to_string(), + function: crate::llm::groq::FunctionCall { + name: tc.name.clone(), + arguments: tc.arguments.to_string(), + }, + }) + .collect(); + + LlmResult { + content: r.content, + tool_calls: r.tool_calls, + raw_tool_calls, + finish_reason: r.stop_reason, + } + } + Err(e) => { + tracing::error!("Claude API error: {}", e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({ "error": format!("LLM API error: {}", e) })), + ) + .into_response(); + } + } + } + }; + + // Check if there are tool calls to execute + if result.tool_calls.is_empty() { + final_response = result.content; + break; + } + + // Add assistant message with tool calls to conversation + messages.push(Message { + role: "assistant".to_string(), + content: result.content.clone(), + tool_calls: Some(result.raw_tool_calls.clone()), + tool_call_id: None, + }); + + // Execute each tool call + for (i, tool_call) in result.tool_calls.iter().enumerate() { + tracing::info!(tool = %tool_call.name, round = round, "Executing discussion tool call"); + + // Parse the tool call + let mut execution_result = parse_discuss_tool_call(tool_call); + + // Handle async discussion tool requests + if let Some(discuss_request) = execution_result.request.take() { + let async_result = + handle_discuss_request(pool, discuss_request, owner_id).await; + execution_result.success = async_result.success; + execution_result.message = async_result.message; + execution_result.data = async_result.data; + + // Check if a contract was created + if let Some(ref data) = execution_result.data { + if let Some(contract_info) = data.get("createdContract") { + created_contract = Some(CreatedContractInfo { + id: contract_info["id"].as_str().unwrap_or("").to_string(), + name: contract_info["name"].as_str().unwrap_or("").to_string(), + description: contract_info["description"].as_str().map(|s| s.to_string()), + contract_type: contract_info["contractType"].as_str().unwrap_or("").to_string(), + initial_phase: contract_info["initialPhase"].as_str().unwrap_or("").to_string(), + }); + } + } + } + + // Check for pending user questions + if let Some(questions) = execution_result.pending_questions { + tracing::info!( + question_count = questions.len(), + "Discussion LLM requesting user input" + ); + pending_questions = Some(questions); + all_tool_call_infos.push(ToolCallInfo { + name: tool_call.name.clone(), + result: ToolResult { + success: execution_result.success, + message: execution_result.message.clone(), + }, + }); + break; + } + + // Build tool result message + let result_content = if let Some(data) = &execution_result.data { + json!({ + "success": execution_result.success, + "message": execution_result.message, + "data": data + }) + .to_string() + } else { + json!({ + "success": execution_result.success, + "message": execution_result.message + }) + .to_string() + }; + + // Add tool result message + let tool_call_id = match llm_client { + LlmClient::Groq(_) => result.raw_tool_calls[i].id.clone(), + LlmClient::Claude(_) => tool_call.id.clone(), + }; + + messages.push(Message { + role: "tool".to_string(), + content: Some(result_content), + tool_calls: None, + tool_call_id: Some(tool_call_id), + }); + + // Track for response + all_tool_call_infos.push(ToolCallInfo { + name: tool_call.name.clone(), + result: ToolResult { + success: execution_result.success, + message: execution_result.message, + }, + }); + } + + // If user questions are pending, pause + if pending_questions.is_some() { + final_response = result.content; + break; + } + + // If finish reason indicates completion, exit loop + let finish_lower = result.finish_reason.to_lowercase(); + if finish_lower == "stop" || finish_lower == "end_turn" { + final_response = result.content; + break; + } + } + + // Build response + let response_text = final_response.unwrap_or_else(|| { + if all_tool_call_infos.is_empty() { + "I couldn't understand your request. Please try rephrasing.".to_string() + } else { + "Done!".to_string() + } + }); + + ( + StatusCode::OK, + Json(DiscussContractResponse { + response: response_text, + tool_calls: all_tool_call_infos, + created_contract, + pending_questions, + }), + ) + .into_response() +} + +/// Result from handling an async discussion tool request +struct DiscussRequestResult { + success: bool, + message: String, + data: Option<serde_json::Value>, +} + +/// Handle async discussion tool requests that require database access +async fn handle_discuss_request( + pool: &sqlx::PgPool, + request: DiscussToolRequest, + owner_id: Uuid, +) -> DiscussRequestResult { + match request { + DiscussToolRequest::CreateContract { + name, + description, + contract_type, + repository_url, + local_only, + } => { + // Create the contract request + let create_req = CreateContractRequest { + name: name.clone(), + description: Some(description.clone()), + contract_type: Some(contract_type.clone()), + template_id: None, + initial_phase: None, + autonomous_loop: None, + phase_guard: None, + local_only: Some(local_only), + auto_merge_local: None, + }; + + match repository::create_contract_for_owner(pool, owner_id, create_req).await { + Ok(contract) => { + // If repository URL was provided, try to add it + if let Some(repo_url) = repository_url { + // Try to add as remote repository + let add_result = repository::add_remote_repository( + pool, + contract.id, + &format!("{} Repository", name), + &repo_url, + true, // is_primary + ) + .await; + + if let Err(e) = add_result { + tracing::warn!( + "Failed to add repository to contract {}: {}", + contract.id, + e + ); + } + } + + DiscussRequestResult { + success: true, + message: format!("Contract '{}' created successfully!", contract.name), + data: Some(json!({ + "createdContract": { + "id": contract.id.to_string(), + "name": contract.name, + "description": contract.description, + "contractType": contract.contract_type, + "initialPhase": contract.phase, + } + })), + } + } + Err(e) => { + tracing::error!("Failed to create contract: {}", e); + DiscussRequestResult { + success: false, + message: format!("Failed to create contract: {}", e), + data: None, + } + } + } + } + } +} diff --git a/makima/src/server/handlers/mod.rs b/makima/src/server/handlers/mod.rs index 3e01a3e..5e172bc 100644 --- a/makima/src/server/handlers/mod.rs +++ b/makima/src/server/handlers/mod.rs @@ -5,6 +5,7 @@ pub mod chains; pub mod chat; pub mod contract_chat; pub mod contract_daemon; +pub mod contract_discuss; pub mod contracts; pub mod file_ws; pub mod files; diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs index d110c18..553797f 100644 --- a/makima/src/server/mod.rs +++ b/makima/src/server/mod.rs @@ -18,7 +18,7 @@ use tower_http::trace::TraceLayer; use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; -use crate::server::handlers::{api_keys, chains, chat, contract_chat, contract_daemon, contracts, file_ws, files, history, listen, mesh, mesh_chat, mesh_daemon, mesh_merge, mesh_supervisor, mesh_ws, repository_history, speak, templates, transcript_analysis, users, versions}; +use crate::server::handlers::{api_keys, chains, chat, contract_chat, contract_daemon, contract_discuss, contracts, file_ws, files, history, listen, mesh, mesh_chat, mesh_daemon, mesh_merge, mesh_supervisor, mesh_ws, repository_history, speak, templates, transcript_analysis, users, versions}; use crate::server::openapi::ApiDoc; use crate::server::state::SharedState; @@ -151,6 +151,7 @@ pub fn make_router(state: SharedState) -> Router { .route("/users/me/password", axum::routing::put(users::change_password_handler)) .route("/users/me/email", axum::routing::put(users::change_email_handler)) // Contract endpoints + .route("/contracts/discuss", post(contract_discuss::discuss_contract_handler)) .route( "/contracts", get(contracts::list_contracts).post(contracts::create_contract), diff --git a/makima/src/server/openapi.rs b/makima/src/server/openapi.rs index f8c5474..a70342b 100644 --- a/makima/src/server/openapi.rs +++ b/makima/src/server/openapi.rs @@ -20,7 +20,7 @@ use crate::server::auth::{ ApiKey, ApiKeyInfoResponse, CreateApiKeyRequest, CreateApiKeyResponse, RefreshApiKeyRequest, RefreshApiKeyResponse, RevokeApiKeyResponse, }; -use crate::server::handlers::{api_keys, contract_chat, contracts, files, listen, mesh, mesh_chat, mesh_merge, repository_history, users}; +use crate::server::handlers::{api_keys, contract_chat, contract_discuss, contracts, files, listen, mesh, mesh_chat, mesh_merge, repository_history, users}; use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage, TranscriptMessage}; #[derive(OpenApi)] @@ -97,6 +97,8 @@ use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage contract_chat::contract_chat_handler, contract_chat::get_contract_chat_history, contract_chat::clear_contract_chat_history, + // Contract discuss endpoint + contract_discuss::discuss_contract_handler, // Repository history/settings endpoints repository_history::list_repository_history, repository_history::get_repository_suggestions, @@ -137,6 +139,12 @@ use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage // Contract chat schemas ContractChatMessageRecord, ContractChatHistoryResponse, + // Contract discuss schemas + contract_discuss::ChatMessage, + contract_discuss::DiscussContractRequest, + contract_discuss::DiscussContractResponse, + contract_discuss::ToolCallInfo, + contract_discuss::CreatedContractInfo, // Merge schemas BranchInfo, BranchListResponse, |
