workflows/default/systems/mcp/tools/task-answer-question/script.ps1
|
# Persist an answered question to /workspace/product/interview-answers.json # Only writes if workspace/product/ exists (i.e. discovery workflow projects) function Write-InterviewAnswer { param( [string]$BotRoot, [hashtable]$Entry # { question_id, question, answer_key, answer_label, answer, context, answered_at } ) $productDir = Join-Path $BotRoot "workspace\product" if (-not (Test-Path $productDir)) { return } $answersPath = Join-Path $productDir "interview-answers.json" $existing = @() if (Test-Path $answersPath) { try { $existing = @((Get-Content $answersPath -Raw | ConvertFrom-Json).answers) } catch {} } # Upsert by question_id $existing = @($existing | Where-Object { $_.question_id -ne $Entry.question_id }) $existing += [PSCustomObject]$Entry @{ answers = $existing } | ConvertTo-Json -Depth 10 | Set-Content $answersPath -Encoding UTF8NoBOM } function Invoke-TaskAnswerQuestion { param( [hashtable]$Arguments ) # Extract arguments $taskId = $Arguments['task_id'] $answer = $Arguments['answer'] $attachments = $Arguments['attachments'] $questionId = $Arguments['question_id'] # Optional: which question to answer (for pending_questions batch) # Validate required fields if (-not $taskId) { throw "Task ID is required" } if (-not $answer) { throw "Answer is required" } # Define tasks directories $tasksBaseDir = Join-Path $global:DotbotProjectRoot ".bot\workspace\tasks" $needsInputDir = Join-Path $tasksBaseDir "needs-input" $analysingDir = Join-Path $tasksBaseDir "analysing" # Find the task file in needs-input $taskFile = $null if (Test-Path $needsInputDir) { $files = Get-ChildItem -Path $needsInputDir -Filter "*.json" -File foreach ($file in $files) { try { $content = Get-Content -Path $file.FullName -Raw | ConvertFrom-Json if ($content.id -eq $taskId) { $taskFile = $file break } } catch { # Continue searching } } } if (-not $taskFile) { throw "Task with ID '$taskId' not found in needs-input status" } # Read task content $taskContent = Get-Content -Path $taskFile.FullName -Raw | ConvertFrom-Json # ----------------------------------------------------------------------- # BATCH PATH: task has pending_questions array (new multi-question format) # ----------------------------------------------------------------------- $hasPendingQuestionsArray = $taskContent.PSObject.Properties['pending_questions'] -and $taskContent.pending_questions -and @($taskContent.pending_questions).Count -gt 0 if ($hasPendingQuestionsArray) { $pendingQuestions = @($taskContent.pending_questions) # Find the target question: by ID if provided, else the first one $targetQuestion = $null $targetIndex = -1 if ($questionId) { for ($i = 0; $i -lt $pendingQuestions.Count; $i++) { if ($pendingQuestions[$i].id -eq $questionId) { $targetQuestion = $pendingQuestions[$i] $targetIndex = $i break } } if (-not $targetQuestion) { throw "Question with ID '$questionId' not found in pending_questions" } } else { $targetQuestion = $pendingQuestions[0] $targetIndex = 0 } # Resolve the answer $resolvedAnswer = $answer $answerType = "custom" $validKeys = @("A", "B", "C", "D", "E") if ($answer.ToUpper() -in $validKeys) { $answerKey = $answer.ToUpper() $answerType = "option" $matchingOption = $targetQuestion.options | Where-Object { $_.key -eq $answerKey } | Select-Object -First 1 if ($matchingOption) { $resolvedAnswer = "$answerKey - $($matchingOption.label)" } else { $resolvedAnswer = $answerKey } } $answeredAt = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'") # Build resolved entry $resolvedEntry = @{ id = $targetQuestion.id question = $targetQuestion.question answer = $resolvedAnswer answer_type = $answerType asked_at = $targetQuestion.asked_at answered_at = $answeredAt } if ($attachments -and $attachments.Count -gt 0) { $resolvedEntry['attachments'] = $attachments } # Persist to interview-answers.json (survives task resets) $interviewEntry = @{ question_id = $targetQuestion.id question = $targetQuestion.question context = $targetQuestion.context answer_key = if ($answerType -eq 'option') { $answerKey } else { $null } answer_label = if ($answerType -eq 'option' -and $matchingOption) { $matchingOption.label } else { $null } answer = $resolvedAnswer answered_at = $answeredAt } Write-InterviewAnswer -BotRoot (Join-Path $global:DotbotProjectRoot '.bot') -Entry $interviewEntry # Add to questions_resolved if (-not $taskContent.PSObject.Properties['questions_resolved']) { $taskContent | Add-Member -NotePropertyName 'questions_resolved' -NotePropertyValue @() -Force } $existingResolved = @($taskContent.questions_resolved) $existingResolved += $resolvedEntry $taskContent.questions_resolved = $existingResolved # Remove this question from pending_questions $remaining = @() for ($i = 0; $i -lt $pendingQuestions.Count; $i++) { if ($i -ne $targetIndex) { $remaining += $pendingQuestions[$i] } } $taskContent.pending_questions = $remaining $taskContent.updated_at = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'") if ($remaining.Count -gt 0) { # More questions remain — stay in needs-input, just update the file $taskContent | ConvertTo-Json -Depth 20 | Set-Content -Path $taskFile.FullName -Encoding UTF8 return @{ success = $true message = "Question answered - $($remaining.Count) question(s) still pending" task_id = $taskId task_name = $taskContent.name old_status = 'needs-input' new_status = 'needs-input' question = $targetQuestion.question answer = $resolvedAnswer answer_type = $answerType questions_resolved_count = $taskContent.questions_resolved.Count questions_remaining_count = $remaining.Count file_path = $taskFile.FullName } } # All questions answered — clear pending_questions and set flag so prompt skips to summary $taskContent.pending_questions = @() if ($taskContent.PSObject.Properties['all_questions_answered']) { $taskContent.all_questions_answered = $true } else { $taskContent | Add-Member -NotePropertyName 'all_questions_answered' -NotePropertyValue $true -Force } # Check skip signal $isSkipAnswer = $resolvedAnswer -match '(?i)skip\s*task|skip\s*-|already\s*exist' if ($isSkipAnswer) { $taskContent.status = 'skipped' if (-not $taskContent.PSObject.Properties['skip_history']) { $taskContent | Add-Member -NotePropertyName 'skip_history' -NotePropertyValue @() -Force } $existingSkips = @($taskContent.skip_history) $existingSkips += @{ skipped_at = $taskContent.updated_at reason = "Skipped via question answer: $resolvedAnswer" } $taskContent.skip_history = $existingSkips $skippedDir = Join-Path $tasksBaseDir "skipped" if (-not (Test-Path $skippedDir)) { New-Item -ItemType Directory -Force -Path $skippedDir | Out-Null } $newFilePath = Join-Path $skippedDir $taskFile.Name $newStatus = 'skipped' $message = "All questions answered - task skipped" } else { $hasCompletedAnalysis = $taskContent.PSObject.Properties['analysis_completed_at'] -and $taskContent.analysis_completed_at -and $taskContent.PSObject.Properties['analysis'] -and $taskContent.analysis if ($hasCompletedAnalysis) { $analysedDir = Join-Path $tasksBaseDir "analysed" if (-not (Test-Path $analysedDir)) { New-Item -ItemType Directory -Force -Path $analysedDir | Out-Null } $taskContent.status = 'analysed' $newFilePath = Join-Path $analysedDir $taskFile.Name $newStatus = 'analysed' $message = "All questions answered - task returned to execution (analysis already complete)" } else { $taskContent.status = 'analysing' if (-not (Test-Path $analysingDir)) { New-Item -ItemType Directory -Force -Path $analysingDir | Out-Null } $newFilePath = Join-Path $analysingDir $taskFile.Name $newStatus = 'analysing' $message = "All questions answered - task returned to analysis" } } $taskContent | ConvertTo-Json -Depth 20 | Set-Content -Path $newFilePath -Encoding UTF8 Remove-Item -Path $taskFile.FullName -Force return @{ success = $true message = $message task_id = $taskId task_name = $taskContent.name old_status = 'needs-input' new_status = $newStatus question = $targetQuestion.question answer = $resolvedAnswer answer_type = $answerType attachments_count = if ($attachments) { @($attachments).Count } else { 0 } questions_resolved_count = $taskContent.questions_resolved.Count file_path = $newFilePath } } # ----------------------------------------------------------------------- # SINGULAR PATH: task has pending_question (legacy single-question format) # ----------------------------------------------------------------------- # Verify there's a pending question if (-not $taskContent.pending_question) { throw "Task has no pending question to answer" } $pendingQuestion = $taskContent.pending_question # Resolve the answer $resolvedAnswer = $answer $answerType = "custom" # Check if answer is an option key $validKeys = @("A", "B", "C", "D", "E") if ($answer.ToUpper() -in $validKeys) { $answerKey = $answer.ToUpper() $answerType = "option" # Find the matching option $matchingOption = $pendingQuestion.options | Where-Object { $_.key -eq $answerKey } | Select-Object -First 1 if ($matchingOption) { $resolvedAnswer = "$answerKey - $($matchingOption.label)" } else { $resolvedAnswer = $answerKey } } $answeredAt = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'") # Create resolved question entry $resolvedEntry = @{ id = $pendingQuestion.id question = $pendingQuestion.question answer = $resolvedAnswer answer_type = $answerType asked_at = $pendingQuestion.asked_at answered_at = $answeredAt } if ($attachments -and $attachments.Count -gt 0) { $resolvedEntry['attachments'] = $attachments } # Persist to interview-answers.json (survives task resets) $singularMatchingOption = if ($answerType -eq 'option') { $pendingQuestion.options | Where-Object { $_.key -eq $answerKey } | Select-Object -First 1 } else { $null } Write-InterviewAnswer -BotRoot (Join-Path $global:DotbotProjectRoot '.bot') -Entry @{ question_id = $pendingQuestion.id question = $pendingQuestion.question context = $pendingQuestion.context answer_key = if ($answerType -eq 'option') { $answerKey } else { $null } answer_label = if ($singularMatchingOption) { $singularMatchingOption.label } else { $null } answer = $resolvedAnswer answered_at = $answeredAt } # Add to questions_resolved array if (-not $taskContent.PSObject.Properties['questions_resolved']) { $taskContent | Add-Member -NotePropertyName 'questions_resolved' -NotePropertyValue @() -Force } # Convert to array if needed and append $existingResolved = @($taskContent.questions_resolved) $existingResolved += $resolvedEntry $taskContent.questions_resolved = $existingResolved # Clear pending question $taskContent.pending_question = $null $taskContent.updated_at = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'") # Check if the answer indicates the task should be skipped $isSkipAnswer = $resolvedAnswer -match '(?i)skip\s*task|skip\s*-|already\s*exist' if ($isSkipAnswer) { # Transition directly to skipped $taskContent.status = 'skipped' # Add skip_history entry if (-not $taskContent.PSObject.Properties['skip_history']) { $taskContent | Add-Member -NotePropertyName 'skip_history' -NotePropertyValue @() -Force } $existingSkips = @($taskContent.skip_history) $existingSkips += @{ skipped_at = $taskContent.updated_at reason = "Skipped via question answer: $resolvedAnswer" } $taskContent.skip_history = $existingSkips $skippedDir = Join-Path $tasksBaseDir "skipped" if (-not (Test-Path $skippedDir)) { New-Item -ItemType Directory -Force -Path $skippedDir | Out-Null } $newFilePath = Join-Path $skippedDir $taskFile.Name $newStatus = 'skipped' $message = "Question answered - task skipped" } else { # If the task already has a completed analysis, skip re-analysis and go straight to execution $hasCompletedAnalysis = $taskContent.PSObject.Properties['analysis_completed_at'] -and $taskContent.analysis_completed_at -and $taskContent.PSObject.Properties['analysis'] -and $taskContent.analysis if ($hasCompletedAnalysis) { $analysedDir = Join-Path $tasksBaseDir "analysed" if (-not (Test-Path $analysedDir)) { New-Item -ItemType Directory -Force -Path $analysedDir | Out-Null } $taskContent.status = 'analysed' $newFilePath = Join-Path $analysedDir $taskFile.Name $newStatus = 'analysed' $message = "Question answered - task returned to execution (analysis already complete)" } else { # No prior analysis — send back to analysing so analysis runs first $taskContent.status = 'analysing' if (-not (Test-Path $analysingDir)) { New-Item -ItemType Directory -Force -Path $analysingDir | Out-Null } $newFilePath = Join-Path $analysingDir $taskFile.Name $newStatus = 'analysing' $message = "Question answered - task returned to analysis" } } # Save updated task to new location $taskContent | ConvertTo-Json -Depth 20 | Set-Content -Path $newFilePath -Encoding UTF8 Remove-Item -Path $taskFile.FullName -Force # Return result return @{ success = $true message = $message task_id = $taskId task_name = $taskContent.name old_status = 'needs-input' new_status = $newStatus question = $pendingQuestion.question answer = $resolvedAnswer answer_type = $answerType attachments_count = if ($attachments) { @($attachments).Count } else { 0 } questions_resolved_count = $taskContent.questions_resolved.Count file_path = $newFilePath } } |