summaryrefslogtreecommitdiff
path: root/makima/src/server/handlers/contract_chat.rs
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/server/handlers/contract_chat.rs')
-rw-r--r--makima/src/server/handlers/contract_chat.rs225
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,
);