tool/Analysis/functions/Invoke-TriageContacts.ps1

function Invoke-TriageContacts {
    <#
    .SYNOPSIS
        Triage contacts to decide which warrant deep web-search research.

    .DESCRIPTION
        Reads contact-research-pass1.json + contact-enrichment.json. For each
        contact, uses Claude Opus to evaluate whether the contact justifies the
        cost of a deep GPT-5.5-pro web-search research pass.

        Decision criteria:
        - Number of "Unknown" fields remaining after Pass 1
        - Whether the company domain has usable website content
        - Seniority level (senior contacts are more findable/valuable)
        - Whether telematics activity signals exist
        - Original engagement quality (sentiment, stage)

        Results are written to contact-triage.json.

    .PARAMETER Pass1Path
        Path to contact-research-pass1.json.

    .PARAMETER EnrichmentPath
        Path to contact-enrichment.json (optional but recommended).

    .PARAMETER AnalysesPath
        Path to email-analyses.json for sentiment context.

    .PARAMETER OutputPath
        Path to data/ directory where contact-triage.json will be written.

    .PARAMETER CachePath
        Directory for response caching.

    .PARAMETER DelayMs
        Delay between API calls in milliseconds. Default: 800.

    .PARAMETER MaxErrors
        Circuit breaker: stop after this many consecutive errors. Default: 5.

    .OUTPUTS
        PSCustomObject with total_contacts, total_selected, total_skipped, total_errors.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Pass1Path,

        [string]$EnrichmentPath,

        [string]$AnalysesPath,

        [Parameter(Mandatory)]
        [string]$OutputPath,

        [string]$CachePath,

        [PSCustomObject]$OwnerProfile,

        [int]$DelayMs = 800,

        [int]$MaxErrors = 5
    )

    # Load Pass 1 research
    $pass1 = Get-Content $Pass1Path -Raw | ConvertFrom-Json

    # Load enrichment if available
    $enrichment = $null
    if ($EnrichmentPath -and (Test-Path $EnrichmentPath)) {
        $enrichment = Get-Content $EnrichmentPath -Raw | ConvertFrom-Json
    }

    # Load analyses if available (for sentiment context)
    $analysesLookup = @{}
    if ($AnalysesPath -and (Test-Path $AnalysesPath)) {
        $analyses = Get-Content $AnalysesPath -Raw | ConvertFrom-Json
        foreach ($a in $analyses.analyses) {
            if ($a.participants) {
                foreach ($p in $a.participants) {
                    if ($p.email -and $p.type -eq 'external') {
                        $key = $p.email.ToLowerInvariant()
                        if (-not $analysesLookup.ContainsKey($key)) {
                            $analysesLookup[$key] = $a
                        }
                    }
                }
            }
        }
    }

    $systemPrompt = @"
You are a strategic research triage analyst for a B2B lead reactivation pipeline.

Your job: decide whether each contact warrants an EXPENSIVE deep web-search research pass
(using GPT-5.5-pro with live web search, costing ~120-150 seconds per contact).

DECISION CRITERIA — select for deep research when:
1. Pass 1 has significant gaps (multiple "Unknown" fields, especially current_title or current_organisation)
2. The company domain has a real website (suggesting the org is findable online)
3. The contact holds or held a senior role (CEO, VP, Director, Head of)
4. There are telematics/IoT signals worth investigating further
5. The original email had warm engagement or was near-close

SKIP deep research when:
1. Pass 1 already found current role + organisation with Medium/High confidence
2. The contact is at a free email provider (gmail, outlook) with no company context
3. The role is junior with low engagement
4. All fields are already populated

Return ONLY valid JSON — no markdown, no preamble, no explanation.
"@


    if ($OwnerProfile) {
        $systemPrompt += "`n`nLEAD-MINING COMPANY CONTEXT:`n- Company: $($OwnerProfile.name)`n- Industry: $($OwnerProfile.industry)"
        if ($OwnerProfile.verticals) {
            $systemPrompt += "`n- Target verticals: $($OwnerProfile.verticals -join '; ')"
        }
        $systemPrompt += "`n`nPrioritise contacts whose organisations operate in verticals aligned with the lead-mining company's offerings."
    }

    $results = @()
    $selectedCount = 0
    $skippedCount = 0
    $errors = @()
    $consecutiveErrors = 0
    $contactCount = 0

    foreach ($contact in $pass1.contacts) {
        $contactCount++

        if ($consecutiveErrors -ge $MaxErrors) {
            Write-Host " Circuit breaker: $MaxErrors consecutive errors, stopping triage" -ForegroundColor Red
            for ($i = $contactCount - 1; $i -lt $pass1.contacts.Count; $i++) {
                $results += [ordered]@{
                    email    = $pass1.contacts[$i].email
                    name     = $pass1.contacts[$i].name
                    selected = $false
                    reason   = 'Skipped (circuit breaker)'
                    error    = 'Circuit breaker'
                }
                $skippedCount++
            }
            break
        }

        # Build context summary for triage
        $unknownCount = 0
        $populatedFields = @()
        if ($contact.research) {
            foreach ($field in @('current_title', 'current_organisation', 'still_in_role', 'linkedin_active')) {
                $val = $contact.research.$field
                if (-not $val -or $val -eq 'Unknown') { $unknownCount++ }
                else { $populatedFields += "$field=$val" }
            }
        }
        else {
            $unknownCount = 4
        }

        $researchSummary = if ($contact.research) {
            @"
- Current title: $($contact.research.current_title)
- Current organisation: $($contact.research.current_organisation)
- Still in role: $($contact.research.still_in_role)
- Telematics activity: found=$($contact.research.telematics_activity.found), summary="$($contact.research.telematics_activity.summary)"
- LinkedIn: $($contact.research.linkedin_active)
- Confidence: $($contact.research.confidence)
- Unknown fields: $unknownCount of 4 key fields
"@

        }
        else { "No research data (all fields Unknown)" }

        # Enrichment context
        $enrichmentContext = "No enrichment data available"
        if ($enrichment) {
            $emailKey = $contact.email.ToLowerInvariant()
            $contactEnrich = $null
            if ($enrichment.contacts.PSObject.Properties.Name -contains $emailKey) {
                $contactEnrich = $enrichment.contacts.$emailKey
            }
            if ($contactEnrich) {
                $domain = $contactEnrich.domain
                $websiteAvail = $contactEnrich.website_available
                $isFree = $contactEnrich.is_free_provider
                $enrichmentContext = "Domain: $domain, Free provider: $isFree, Website available: $websiteAvail"

                if (-not $isFree -and $enrichment.domains.PSObject.Properties.Name -contains $domain) {
                    $domainInfo = $enrichment.domains.$domain
                    if ($domainInfo.pages) {
                        $pageCount = @($domainInfo.pages.PSObject.Properties | Where-Object { $_.Value.success -eq $true }).Count
                        $enrichmentContext += ", Scraped pages with content: $pageCount"
                    }
                }
            }
        }

        # Sentiment context
        $sentimentContext = "No email analysis available"
        $emailKey2 = $contact.email.ToLowerInvariant()
        if ($analysesLookup.ContainsKey($emailKey2)) {
            $analysis = $analysesLookup[$emailKey2]
            if ($analysis.sentiment) {
                $sentimentContext = "Tone: $($analysis.sentiment.overall_tone), Engagement: $($analysis.sentiment.engagement_level), Conclusion: $($analysis.sentiment.conclusion)"
            }
            if ($analysis.opportunity) {
                $sentimentContext += ", Stage: $($analysis.opportunity.stage), Category: $(($analysis.opportunity.category) -join ', ')"
            }
        }

        $userPrompt = @"
Evaluate whether this contact should receive deep web-search research:

## Contact
- Name: $($contact.name)
- Email: $($contact.email)
- Original role: $($contact.original_role)
- Original organisation: $($contact.original_organisation)
- Email date: $($contact.email_date)

## Pass 1 Research Results
$researchSummary

## OSINT Enrichment
$enrichmentContext

## Original Email Context
$sentimentContext

## Required Output (JSON)
{
  "selected": true or false,
  "confidence": "High|Medium|Low",
  "reason": "1-2 sentence explanation of why this contact should or should not receive deep research",
  "priority": 1-5 (1=highest priority for deep research, 5=lowest)
}
"@


        Write-Host " [$contactCount/$($pass1.contacts.Count)] $($contact.name)" -NoNewline -ForegroundColor Gray

        try {
            $aiParams = @{
                SystemPrompt = $systemPrompt
                UserPrompt   = $userPrompt
                JsonMode     = $true
                MaxTokens    = 500
                Provider     = 'anthropic'
            }

            if ($CachePath) {
                $cacheKey = ($contact.email -replace '[^\w\-\.]', '_')
                $aiParams.CachePath = $CachePath
                $aiParams.CacheKey = $cacheKey
                $response = Invoke-PCCompletionCached @aiParams
            }
            else {
                $response = Invoke-PCCompletion @aiParams
            }
            if (-not $response) { throw 'Empty response from AI' }

            $parsed = Repair-JsonResponse -Response $response

            $isSelected = [bool]$parsed.selected
            $results += [ordered]@{
                email      = $contact.email
                name       = $contact.name
                selected   = $isSelected
                confidence = $parsed.confidence
                reason     = $parsed.reason
                priority   = [int]$parsed.priority
                error      = $null
            }

            if ($isSelected) {
                $selectedCount++
                Write-Host " → SELECTED (P$($parsed.priority))" -ForegroundColor Cyan
            }
            else {
                $skippedCount++
                Write-Host " → skip" -ForegroundColor DarkGray
            }
            $consecutiveErrors = 0
        }
        catch {
            $consecutiveErrors++
            $results += [ordered]@{
                email    = $contact.email
                name     = $contact.name
                selected = $false
                reason   = "Triage error: $($_.Exception.Message)"
                error    = $_.Exception.Message
            }
            $errors += [ordered]@{ contact = $contact.email; error = $_.Exception.Message }
            $skippedCount++
            Write-Host " ERROR" -ForegroundColor Red
        }

        if ($contactCount -lt $pass1.contacts.Count) {
            Start-Sleep -Milliseconds $DelayMs
        }
    }

    # Sort selected contacts by priority
    $selectedContacts = @($results | Where-Object { $_.selected } | Sort-Object { $_.priority })

    # Write output
    $output = [ordered]@{
        metadata = [ordered]@{
            generated_at   = (Get-Date).ToString('o')
            total_contacts = $pass1.contacts.Count
            total_selected = $selectedCount
            total_skipped  = $skippedCount
            total_errors   = $errors.Count
            selection_rate = if ($pass1.contacts.Count -gt 0) { [math]::Round($selectedCount / $pass1.contacts.Count * 100, 1) } else { 0 }
        }
        selected = $selectedContacts
        all      = $results
    }

    $outputFile = Join-Path $OutputPath 'contact-triage.json'
    $output | ConvertTo-Json -Depth 10 | Set-Content -Path $outputFile -Encoding UTF8

    Write-Host " Selected $selectedCount/$($pass1.contacts.Count) for deep research ($($output.metadata.selection_rate)%)" -ForegroundColor $(if ($selectedCount -gt 0) { 'Green' } else { 'Yellow' })

    [PSCustomObject]@{
        total_contacts = $pass1.contacts.Count
        total_selected = $selectedCount
        total_skipped  = $skippedCount
        total_errors   = $errors.Count
        output_path    = $outputFile
    }
}