diff options
Diffstat (limited to 'makima/src/server')
| -rw-r--r-- | makima/src/server/handlers/contract_chat.rs | 323 |
1 files changed, 323 insertions, 0 deletions
diff --git a/makima/src/server/handlers/contract_chat.rs b/makima/src/server/handlers/contract_chat.rs index d090999..557e27b 100644 --- a/makima/src/server/handlers/contract_chat.rs +++ b/makima/src/server/handlers/contract_chat.rs @@ -25,6 +25,8 @@ use crate::llm::{ groq::{GroqClient, GroqError, Message, ToolCallResponse}, parse_contract_tool_call, templates_for_phase, ContractToolRequest, FileInfo, LlmModel, TaskInfo, ToolCall, ToolResult, UserQuestion, CONTRACT_TOOLS, + format_transcript_for_analysis, calculate_speaker_stats, + build_analysis_prompt, parse_analysis_response, }; use crate::server::auth::Authenticated; use crate::server::state::{DaemonCommand, SharedState}; @@ -2201,6 +2203,327 @@ async fn handle_contract_request( }, } } + + // ============================================================================= + // Transcript Analysis Handlers + // ============================================================================= + + ContractToolRequest::AnalyzeTranscript { file_id } => { + // Get the file + let file = match repository::get_file_for_owner(pool, file_id, owner_id).await { + Ok(Some(f)) => f, + Ok(None) => { + return ContractRequestResult { + success: false, + message: "File not found".to_string(), + data: None, + }; + } + Err(e) => { + return ContractRequestResult { + success: false, + message: format!("Database error: {}", e), + data: None, + }; + } + }; + + if file.transcript.is_empty() { + return ContractRequestResult { + success: false, + message: "File has no transcript to analyze".to_string(), + data: None, + }; + } + + // Format and analyze + let transcript_text = format_transcript_for_analysis(&file.transcript); + let speaker_stats = calculate_speaker_stats(&file.transcript); + let prompt = build_analysis_prompt(&transcript_text); + + // Call Claude for analysis + let client = match ClaudeClient::from_env(ClaudeModel::Sonnet) { + Ok(c) => c, + Err(e) => { + return ContractRequestResult { + success: false, + message: format!("Failed to create Claude client: {}", e), + data: None, + }; + } + }; + + let claude_messages = vec![claude::Message { + role: "user".to_string(), + content: claude::MessageContent::Text(prompt), + }]; + + match client.chat_with_tools(claude_messages, &[]).await { + Ok(result) => { + let response_content = result.content.unwrap_or_default(); + match parse_analysis_response(&response_content, speaker_stats) { + Ok(analysis) => { + ContractRequestResult { + success: true, + message: format!( + "Analysis complete: {} requirements, {} decisions, {} action items", + analysis.requirements.len(), + analysis.decisions.len(), + analysis.action_items.len() + ), + data: Some(json!({ + "analysis": analysis + })), + } + } + Err(e) => ContractRequestResult { + success: false, + message: format!("Failed to parse analysis: {}", e), + data: None, + } + } + } + Err(e) => ContractRequestResult { + success: false, + message: format!("Claude API error: {}", e), + data: None, + } + } + } + + ContractToolRequest::CreateContractFromTranscript { + file_id, name, description, include_requirements, include_decisions, include_action_items + } => { + // Get file + let file = match repository::get_file_for_owner(pool, file_id, owner_id).await { + Ok(Some(f)) => f, + Ok(None) => { + return ContractRequestResult { + success: false, + message: "File not found".to_string(), + data: None, + }; + } + Err(e) => { + return ContractRequestResult { + success: false, + message: format!("Database error: {}", e), + data: None, + }; + } + }; + + if file.transcript.is_empty() { + return ContractRequestResult { + success: false, + message: "File has no transcript".to_string(), + data: None, + }; + } + + // Analyze transcript + let transcript_text = format_transcript_for_analysis(&file.transcript); + let speaker_stats = calculate_speaker_stats(&file.transcript); + let prompt = build_analysis_prompt(&transcript_text); + + let client = match ClaudeClient::from_env(ClaudeModel::Sonnet) { + Ok(c) => c, + Err(e) => { + return ContractRequestResult { + success: false, + message: format!("Failed to create Claude client: {}", e), + data: None, + }; + } + }; + + let claude_messages = vec![claude::Message { + role: "user".to_string(), + content: claude::MessageContent::Text(prompt), + }]; + + let analysis = match client.chat_with_tools(claude_messages, &[]).await { + Ok(result) => { + let response_content = result.content.unwrap_or_default(); + match parse_analysis_response(&response_content, speaker_stats) { + Ok(a) => a, + Err(e) => { + return ContractRequestResult { + success: false, + message: format!("Failed to parse analysis: {}", e), + data: None, + }; + } + } + } + Err(e) => { + return ContractRequestResult { + success: false, + message: format!("Claude API error: {}", e), + data: None, + }; + } + }; + + // Create contract + let contract_name = name + .or(analysis.suggested_contract_name.clone()) + .unwrap_or_else(|| format!("Contract from {}", file.name)); + let contract_description = description.or(analysis.suggested_description.clone()); + + let contract_req = crate::db::models::CreateContractRequest { + name: contract_name.clone(), + description: contract_description, + initial_phase: Some("research".to_string()), + }; + + let contract = match repository::create_contract_for_owner(pool, owner_id, contract_req).await { + Ok(c) => c, + Err(e) => { + return ContractRequestResult { + success: false, + message: format!("Failed to create contract: {}", e), + data: None, + }; + } + }; + + let mut files_created = 0; + let mut tasks_created = 0; + + // Create requirements file if requested and there are requirements + if include_requirements && !analysis.requirements.is_empty() { + let requirements_items: Vec<String> = analysis.requirements + .iter() + .map(|req| format!("[{}] {}", req.speaker, req.text)) + .collect(); + + let body: Vec<crate::db::models::BodyElement> = vec![ + crate::db::models::BodyElement::Heading { + level: 1, + text: "Requirements".to_string(), + }, + crate::db::models::BodyElement::Paragraph { + text: format!("Extracted {} requirements from transcript analysis.", analysis.requirements.len()), + }, + crate::db::models::BodyElement::Heading { + level: 2, + text: "Extracted Requirements".to_string(), + }, + crate::db::models::BodyElement::List { + ordered: false, + items: requirements_items, + }, + ]; + + let create_req = crate::db::models::CreateFileRequest { + contract_id: contract.id, + name: Some("Requirements".to_string()), + description: Some("Requirements extracted from transcript analysis".to_string()), + body, + transcript: Vec::new(), + location: None, + repo_file_path: None, + contract_phase: Some("specify".to_string()), + }; + + if repository::create_file_for_owner(pool, owner_id, create_req).await.is_ok() { + files_created += 1; + } + } + + // Create decisions file if requested and there are decisions + if include_decisions && !analysis.decisions.is_empty() { + let decisions_items: Vec<String> = analysis.decisions + .iter() + .map(|dec| format!("[{}] {}", dec.speaker, dec.text)) + .collect(); + + let body: Vec<crate::db::models::BodyElement> = vec![ + crate::db::models::BodyElement::Heading { + level: 1, + text: "Decisions".to_string(), + }, + crate::db::models::BodyElement::Paragraph { + text: format!("Extracted {} decisions from transcript analysis.", analysis.decisions.len()), + }, + crate::db::models::BodyElement::Heading { + level: 2, + text: "Recorded Decisions".to_string(), + }, + crate::db::models::BodyElement::List { + ordered: false, + items: decisions_items, + }, + ]; + + let create_req = crate::db::models::CreateFileRequest { + contract_id: contract.id, + name: Some("Decisions".to_string()), + description: Some("Decisions extracted from transcript analysis".to_string()), + body, + transcript: Vec::new(), + location: None, + repo_file_path: None, + contract_phase: Some("research".to_string()), + }; + + if repository::create_file_for_owner(pool, owner_id, create_req).await.is_ok() { + files_created += 1; + } + } + + // Create tasks from action items if requested + if include_action_items && !analysis.action_items.is_empty() { + for item in &analysis.action_items { + let task_req = CreateTaskRequest { + contract_id: contract.id, + name: item.text.chars().take(100).collect(), + description: Some(format!("Action item from: {}", item.speaker)), + plan: item.text.clone(), + parent_task_id: None, + repository_url: None, + base_branch: None, + target_branch: None, + merge_mode: None, + priority: match item.priority.as_deref() { + Some("high") => 10, + Some("medium") => 5, + _ => 0, + }, + target_repo_path: None, + completion_action: None, + continue_from_task_id: None, + copy_files: None, + is_supervisor: false, + checkpoint_sha: None, + }; + + if repository::create_task_for_owner(pool, owner_id, task_req).await.is_ok() { + tasks_created += 1; + } + } + } + + ContractRequestResult { + success: true, + message: format!( + "Created contract '{}' with {} files and {} tasks from transcript analysis", + contract_name, files_created, tasks_created + ), + data: Some(json!({ + "contractId": contract.id, + "contractName": contract_name, + "filesCreated": files_created, + "tasksCreated": tasks_created, + "analysis": { + "requirementsCount": analysis.requirements.len(), + "decisionsCount": analysis.decisions.len(), + "actionItemsCount": analysis.action_items.len() + } + })), + } + } } } |
