diff options
| author | soryu <soryu@soryu.co> | 2026-01-26 20:19:30 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-26 20:19:30 +0000 |
| commit | 04e1e8f0dd85d19917ac5ba0b73cba65ebac8976 (patch) | |
| tree | e52537dd2a33c10156f1378ffdc6803bc983482d /makima/src/server/handlers/contract_chat.rs | |
| parent | 6328477bc459eca0243b685553dbd75b925fdc8a (diff) | |
| download | soryu-04e1e8f0dd85d19917ac5ba0b73cba65ebac8976.tar.gz soryu-04e1e8f0dd85d19917ac5ba0b73cba65ebac8976.zip | |
Add completion phases
Diffstat (limited to 'makima/src/server/handlers/contract_chat.rs')
| -rw-r--r-- | makima/src/server/handlers/contract_chat.rs | 225 |
1 files changed, 87 insertions, 138 deletions
diff --git a/makima/src/server/handlers/contract_chat.rs b/makima/src/server/handlers/contract_chat.rs index 28c3436..e035368 100644 --- a/makima/src/server/handlers/contract_chat.rs +++ b/makima/src/server/handlers/contract_chat.rs @@ -19,11 +19,11 @@ use crate::db::{ repository, }; use crate::llm::{ - all_templates, analyze_task_output, body_to_markdown, format_checklist_markdown, + analyze_task_output, body_to_markdown, format_checklist_markdown, format_parsed_tasks, parse_tasks_from_breakdown, claude::{self, ClaudeClient, ClaudeError, ClaudeModel}, groq::{GroqClient, GroqError, Message, ToolCallResponse}, - parse_contract_tool_call, templates_for_phase, ContractToolRequest, FileInfo, + parse_contract_tool_call, ContractToolRequest, LlmModel, TaskInfo, ToolCall, ToolResult, UserQuestion, CONTRACT_TOOLS, format_transcript_for_analysis, calculate_speaker_stats, build_analysis_prompt, parse_analysis_response, @@ -441,36 +441,29 @@ fn build_contract_context(contract: &crate::db::models::ContractWithRelations) - context.push_str(&format!("Description: {}\n", desc)); } - // Build phase checklist - let file_infos: Vec<FileInfo> = contract.files.iter().map(|f| FileInfo { - id: f.id, - name: f.name.clone(), - contract_phase: f.contract_phase.clone(), - }).collect(); + // Get completed deliverables for the current phase + let completed_deliverables = c.get_completed_deliverables(&c.phase); + // Build task infos for checklist let task_infos: Vec<TaskInfo> = contract.tasks.iter().map(|t| TaskInfo { - id: t.id, name: t.name.clone(), status: t.status.clone(), }).collect(); let has_repository = !contract.repositories.is_empty(); - let phase_checklist = crate::llm::get_phase_checklist_for_type(&c.phase, &file_infos, &task_infos, has_repository, &c.contract_type); + let phase_checklist = crate::llm::get_phase_checklist_for_type(&c.phase, &completed_deliverables, &task_infos, has_repository, &c.contract_type); // Add phase checklist to context context.push_str("\n"); context.push_str(&format_checklist_markdown(&phase_checklist)); // Add deliverable check result for phase transition readiness - // Note: pr_url is not available in TaskSummary, so we pass None here - // Full PR checking should be done via the check_deliverables_met tool let deliverable_check = crate::llm::check_deliverables_met( &c.phase, &c.contract_type, - &file_infos, + &completed_deliverables, &task_infos, has_repository, - None, // pr_url not available in TaskSummary ); // Add deliverable prompt guidance @@ -1204,23 +1197,7 @@ async fn handle_contract_request( } } - ContractToolRequest::CreateFileFromTemplate { - template_id, - name, - description, - } => { - // Find the template - let templates = all_templates(); - let template = templates.iter().find(|t| t.id == template_id); - - let Some(template) = template else { - return ContractRequestResult { - success: false, - message: format!("Template '{}' not found", template_id), - data: None, - }; - }; - + ContractToolRequest::CreateEmptyFile { name, description } => { // Verify contract exists and get current phase let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await { Ok(Some(c)) => c, @@ -1240,32 +1217,25 @@ async fn handle_contract_request( } }; - // Use template's phase if available, otherwise use contract's current phase - let contract_phase = Some(template.phase.clone()).or(Some(contract.phase.clone())); - - // Create the file (contract_id is now required) + // Create the file with current contract phase let create_req = crate::db::models::CreateFileRequest { contract_id, name: Some(name.clone()), description, - body: template.suggested_body.clone(), + body: Vec::new(), transcript: Vec::new(), location: None, repo_file_path: None, - contract_phase, + contract_phase: Some(contract.phase.clone()), }; match repository::create_file_for_owner(pool, owner_id, create_req).await { Ok(file) => ContractRequestResult { success: true, - message: format!( - "Created file '{}' from template '{}'", - name, template.name - ), + message: format!("Created empty file '{}'", name), data: Some(json!({ "fileId": file.id, "name": file.name, - "templateId": template_id, })), }, Err(e) => ContractRequestResult { @@ -1276,8 +1246,11 @@ async fn handle_contract_request( } } - ContractToolRequest::CreateEmptyFile { name, description } => { - // Verify contract exists and get current phase + ContractToolRequest::MarkDeliverableComplete { + deliverable_id, + phase, + } => { + // Get the contract to determine current phase and contract type let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await { Ok(Some(c)) => c, Ok(None) => { @@ -1296,61 +1269,60 @@ async fn handle_contract_request( } }; - // Create the file with current contract phase - let create_req = crate::db::models::CreateFileRequest { - contract_id, - name: Some(name.clone()), - description, - body: Vec::new(), - transcript: Vec::new(), - location: None, - repo_file_path: None, - contract_phase: Some(contract.phase.clone()), - }; + // Use specified phase or default to current contract phase + let target_phase = phase.unwrap_or_else(|| contract.phase.clone()); - match repository::create_file_for_owner(pool, owner_id, create_req).await { - Ok(file) => ContractRequestResult { + // Validate the deliverable ID exists for this phase/contract type + let phase_deliverables = crate::llm::get_phase_deliverables_for_type(&target_phase, &contract.contract_type); + let deliverable_exists = phase_deliverables.deliverables.iter().any(|d| d.id == deliverable_id); + + if !deliverable_exists { + let valid_ids: Vec<&str> = phase_deliverables.deliverables.iter().map(|d| d.id.as_str()).collect(); + return ContractRequestResult { + success: false, + message: format!( + "Invalid deliverable_id '{}' for {} phase. Valid IDs: {:?}", + deliverable_id, target_phase, valid_ids + ), + data: None, + }; + } + + // Check if already completed + if contract.is_deliverable_complete(&target_phase, &deliverable_id) { + return ContractRequestResult { success: true, - message: format!("Created empty file '{}'", name), + message: format!("Deliverable '{}' is already marked complete for {} phase", deliverable_id, target_phase), data: Some(json!({ - "fileId": file.id, - "name": file.name, + "deliverableId": deliverable_id, + "phase": target_phase, + "alreadyComplete": true, })), - }, + }; + } + + // Mark the deliverable as complete + match repository::mark_deliverable_complete(pool, contract_id, &target_phase, &deliverable_id).await { + Ok(updated_contract) => { + let completed = updated_contract.get_completed_deliverables(&target_phase); + ContractRequestResult { + success: true, + message: format!("Marked deliverable '{}' as complete for {} phase", deliverable_id, target_phase), + data: Some(json!({ + "deliverableId": deliverable_id, + "phase": target_phase, + "completedDeliverables": completed, + })), + } + } Err(e) => ContractRequestResult { success: false, - message: format!("Failed to create file: {}", e), + message: format!("Failed to mark deliverable complete: {}", e), data: None, }, } } - ContractToolRequest::ListAvailableTemplates { phase } => { - let templates = if let Some(p) = phase { - templates_for_phase(&p) - } else { - all_templates() - }; - - let template_data: Vec<serde_json::Value> = templates - .iter() - .map(|t| { - json!({ - "id": t.id, - "name": t.name, - "phase": t.phase, - "description": t.description, - }) - }) - .collect(); - - ContractRequestResult { - success: true, - message: format!("Found {} templates", templates.len()), - data: Some(json!({ "templates": template_data })), - } - } - ContractToolRequest::CreateContractTask { name, plan, @@ -1666,8 +1638,8 @@ async fn handle_contract_request( }; let phase_info = get_phase_description(&contract.phase); - let templates = templates_for_phase(&contract.phase); - let template_names: Vec<String> = templates.iter().map(|t| t.name.clone()).collect(); + let phase_deliverables = crate::llm::get_phase_deliverables_for_type(&contract.phase, &contract.contract_type); + let deliverable_names: Vec<String> = phase_deliverables.deliverables.iter().map(|d| d.name.clone()).collect(); ContractRequestResult { success: true, @@ -1676,7 +1648,8 @@ async fn handle_contract_request( "phase": contract.phase, "description": phase_info.0, "activities": phase_info.1, - "suggestedTemplates": template_names, + "deliverables": deliverable_names, + "guidance": phase_deliverables.guidance, "nextPhase": get_next_phase(&contract.phase), })), } @@ -1764,30 +1737,22 @@ async fn handle_contract_request( } }; - let file_infos: Vec<FileInfo> = cwr.files.iter().map(|f| FileInfo { - id: f.id, - name: f.name.clone(), - contract_phase: f.contract_phase.clone(), - }).collect(); + // Get completed deliverables for the current phase + let completed_deliverables = cwr.contract.get_completed_deliverables(current_phase); let task_infos: Vec<TaskInfo> = cwr.tasks.iter().map(|t| TaskInfo { - id: t.id, name: t.name.clone(), status: t.status.clone(), }).collect(); let has_repository = !cwr.repositories.is_empty(); - // Note: pr_url is not available in TaskSummary, so we skip PR checking here - // For simple contracts, the PR deliverable check will need to be done - // by fetching full task details if needed let check_result = crate::llm::check_deliverables_met( current_phase, &contract.contract_type, - &file_infos, + &completed_deliverables, &task_infos, has_repository, - None, // pr_url not available in TaskSummary ); // Block transition if deliverables are not met @@ -1895,17 +1860,17 @@ async fn handle_contract_request( match repository::change_contract_phase_for_owner(pool, contract_id, owner_id, &new_phase).await { Ok(Some(updated)) => { // Get deliverables for the new phase (using contract type) - let deliverables = crate::llm::get_phase_deliverables_for_type(&new_phase, &contract.contract_type); + let phase_deliverables = crate::llm::get_phase_deliverables_for_type(&new_phase, &contract.contract_type); - // Build suggested files list - let suggested_files: Vec<serde_json::Value> = deliverables - .recommended_files + // Build deliverables list + let deliverables_list: Vec<serde_json::Value> = phase_deliverables + .deliverables .iter() - .map(|f| json!({ - "templateId": f.template_id, - "name": f.name_suggestion, - "priority": format!("{:?}", f.priority).to_lowercase(), - "description": f.description, + .map(|d| json!({ + "id": d.id, + "name": d.name, + "priority": format!("{:?}", d.priority).to_lowercase(), + "description": d.description, })) .collect(); @@ -1913,16 +1878,16 @@ async fn handle_contract_request( success: true, message: format!( "Advanced contract from '{}' to '{}' phase. {}", - current_phase, new_phase, deliverables.guidance + current_phase, new_phase, phase_deliverables.guidance ), data: Some(json!({ "status": "advanced", "previousPhase": current_phase, "newPhase": updated.phase, - "phaseGuidance": deliverables.guidance, - "suggestedFiles": suggested_files, - "requiresRepository": deliverables.requires_repository, - "requiresTasks": deliverables.requires_tasks, + "phaseGuidance": phase_deliverables.guidance, + "deliverables": deliverables_list, + "requiresRepository": phase_deliverables.requires_repository, + "requiresTasks": phase_deliverables.requires_tasks, })), } }, @@ -2028,20 +1993,15 @@ async fn handle_contract_request( ContractToolRequest::GetPhaseChecklist => { match get_contract_with_relations(pool, contract_id, owner_id).await { Ok(Some(cwr)) => { - let file_infos: Vec<FileInfo> = cwr.files.iter().map(|f| FileInfo { - id: f.id, - name: f.name.clone(), - contract_phase: f.contract_phase.clone(), - }).collect(); + let completed_deliverables = cwr.contract.get_completed_deliverables(&cwr.contract.phase); let task_infos: Vec<TaskInfo> = cwr.tasks.iter().map(|t| TaskInfo { - id: t.id, name: t.name.clone(), status: t.status.clone(), }).collect(); let has_repository = !cwr.repositories.is_empty(); - let checklist = crate::llm::get_phase_checklist_for_type(&cwr.contract.phase, &file_infos, &task_infos, has_repository, &cwr.contract.contract_type); + let checklist = crate::llm::get_phase_checklist_for_type(&cwr.contract.phase, &completed_deliverables, &task_infos, has_repository, &cwr.contract.contract_type); ContractRequestResult { success: true, @@ -2049,7 +2009,7 @@ async fn handle_contract_request( data: Some(json!({ "phase": checklist.phase, "completionPercentage": checklist.completion_percentage, - "deliverables": checklist.file_deliverables, + "deliverables": checklist.deliverables, "hasRepository": checklist.has_repository, "repositoryRequired": checklist.repository_required, "taskStats": checklist.task_stats, @@ -2074,41 +2034,30 @@ async fn handle_contract_request( ContractToolRequest::CheckDeliverablesMet => { match get_contract_with_relations(pool, contract_id, owner_id).await { Ok(Some(cwr)) => { - let file_infos: Vec<FileInfo> = cwr.files.iter().map(|f| FileInfo { - id: f.id, - name: f.name.clone(), - contract_phase: f.contract_phase.clone(), - }).collect(); + let completed_deliverables = cwr.contract.get_completed_deliverables(&cwr.contract.phase); let task_infos: Vec<TaskInfo> = cwr.tasks.iter().map(|t| TaskInfo { - id: t.id, name: t.name.clone(), status: t.status.clone(), }).collect(); let has_repository = !cwr.repositories.is_empty(); - // Note: pr_url is not available in TaskSummary - // For simple contracts needing PR checking, full task details would need to be fetched - // For now, we pass None and the LLM can guide the user to ensure a PR exists - let check_result = crate::llm::check_deliverables_met( &cwr.contract.phase, &cwr.contract.contract_type, - &file_infos, + &completed_deliverables, &task_infos, has_repository, - None, // pr_url not available in TaskSummary ); // Check if we should auto-progress let auto_progress = crate::llm::should_auto_progress( &cwr.contract.phase, &cwr.contract.contract_type, - &file_infos, + &completed_deliverables, &task_infos, has_repository, - None, // pr_url not available in TaskSummary cwr.contract.autonomous_loop, ); |
