Public/Get-UserTicketHistory.ps1
|
function Get-UserTicketHistory { <# .SYNOPSIS Retrieves all tickets for a user (as requester, assignee, or mentioned) and generates an AI summary. .DESCRIPTION Pulls tickets where the specified user was the requester, the assignee, or mentioned in ticket notes. Uses AI to summarize interaction patterns, workload distribution, and common issue types. .EXAMPLE Get-UserTicketHistory -UserIdentity 'john.doe@company.com' -Provider ServiceNow -Instance 'company.service-now.com' -Credential $cred .EXAMPLE Get-UserTicketHistory -UserIdentity 'Jane Smith' -Provider File -FilePath '.\tickets.csv' -Role Assignee #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$UserIdentity, [Parameter()] [ValidateSet('All', 'Requester', 'Assignee', 'Both')] [string]$Role = 'All', [Parameter()] [ValidateSet('ServiceNow', 'Jira', 'File')] [string]$Provider = 'ServiceNow', [Parameter()] [string]$Instance, [Parameter()] [PSCredential]$Credential, [Parameter()] [string]$ApiKey, [Parameter()] [string]$BaseUrl, [Parameter()] [string]$Email, [Parameter()] [string]$FilePath, [Parameter()] [ValidateRange(1, 60)] [int]$MonthsBack = 12, [Parameter()] [ValidateSet('Anthropic', 'OpenAI', 'Ollama', 'Custom')] [string]$AIProvider = 'Anthropic', [Parameter()] [string]$AIApiKey, [Parameter()] [string]$AIModel, [Parameter()] [string]$AIEndpoint, [Parameter()] [string]$OutputPath, [Parameter()] [switch]$SkipAI ) Write-Verbose "Getting ticket history for user '$UserIdentity' (Role: $Role, Provider: $Provider, last $MonthsBack months)" $allTickets = @() $ticketsByRole = @{ Requester = @() Assignee = @() Mentioned = @() } switch ($Provider) { 'ServiceNow' { if (-not $Instance) { throw 'ServiceNow provider requires -Instance parameter.' } $authParams = @{ Instance = $Instance } if ($Credential) { $authParams['Credential'] = $Credential } if ($ApiKey) { $authParams['ApiKey'] = $ApiKey } $fields = 'number,short_description,description,state,priority,category,subcategory,opened_at,closed_at,resolved_at,assigned_to,caller_id,close_notes,work_notes,sys_class_name' $dateFilter = "sys_created_on>javascript:gs.monthsAgo($MonthsBack)" # As Requester (caller) if ($Role -in @('All', 'Requester', 'Both')) { Write-Verbose 'Querying tickets where user is requester...' $callerQuery = "caller_id.name=$UserIdentity^$dateFilter" $callerTickets = Connect-ServiceNow @authParams -Method GET -Endpoint 'api/now/table/incident' ` -QueryParameters @{ sysparm_query = $callerQuery sysparm_fields = $fields sysparm_limit = '10000' } -Paginate foreach ($t in @($callerTickets)) { $ticket = Convert-SNOWTicketToObject -Ticket $t -UserIdentity $UserIdentity $ticketsByRole['Requester'] += $ticket } Write-Verbose "Found $(@($callerTickets).Count) tickets as requester" } # As Assignee if ($Role -in @('All', 'Assignee', 'Both')) { Write-Verbose 'Querying tickets where user is assignee...' # Query across incident, change_request, and problem tables foreach ($table in @('incident', 'change_request', 'problem')) { $assigneeQuery = "assigned_to.name=$UserIdentity^$dateFilter" $assignedTickets = Connect-ServiceNow @authParams -Method GET -Endpoint "api/now/table/$table" ` -QueryParameters @{ sysparm_query = $assigneeQuery sysparm_fields = $fields sysparm_limit = '10000' } -Paginate foreach ($t in @($assignedTickets)) { $ticket = Convert-SNOWTicketToObject -Ticket $t -UserIdentity $UserIdentity $ticketsByRole['Assignee'] += $ticket } Write-Verbose "Found $(@($assignedTickets).Count) assigned tickets in $table" } } # Mentioned in watch list (All role only) if ($Role -eq 'All') { Write-Verbose 'Querying tickets where user is in watch list...' $watchQuery = "watch_listLIKE$UserIdentity^$dateFilter" $watchTickets = Connect-ServiceNow @authParams -Method GET -Endpoint 'api/now/table/incident' ` -QueryParameters @{ sysparm_query = $watchQuery sysparm_fields = $fields sysparm_limit = '5000' } -Paginate foreach ($t in @($watchTickets)) { $ticket = Convert-SNOWTicketToObject -Ticket $t -UserIdentity $UserIdentity # Only add if not already captured as requester or assignee $existingNumbers = ($ticketsByRole['Requester'] + $ticketsByRole['Assignee']) | ForEach-Object { $_.Number } if ($ticket.Number -notin $existingNumbers) { $ticketsByRole['Mentioned'] += $ticket } } Write-Verbose "Found $(@($watchTickets).Count) tickets in watch list" } } 'Jira' { if (-not $BaseUrl) { throw 'Jira provider requires -BaseUrl parameter.' } if (-not $Email) { throw 'Jira provider requires -Email parameter.' } $jiraAuthParams = @{ BaseUrl = $BaseUrl Email = $Email } if ($ApiKey) { $jiraAuthParams['ApiToken'] = $ApiKey } $jqlParts = @() if ($Role -in @('All', 'Requester', 'Both')) { $jqlParts += "reporter = ""$UserIdentity""" } if ($Role -in @('All', 'Assignee', 'Both')) { $jqlParts += "assignee = ""$UserIdentity""" } if ($Role -eq 'All') { $jqlParts += "text ~ ""$UserIdentity""" } $jql = "($($jqlParts -join ' OR ')) AND created >= ""-${MonthsBack}m""" Write-Verbose "Querying Jira: $jql" $jiraIssues = Connect-JiraSM @jiraAuthParams -Method GET -Endpoint 'rest/api/3/search' ` -QueryParameters @{ jql = $jql maxResults = '100' fields = 'summary,description,status,priority,issuetype,created,resolutiondate,assignee,reporter,comment,components' } -Paginate foreach ($issue in @($jiraIssues)) { $ticket = ConvertFrom-JiraIssue -Issue $issue $reporter = if ($issue.fields.reporter) { $issue.fields.reporter.displayName } else { '' } $assignee = if ($issue.fields.assignee) { $issue.fields.assignee.displayName } else { '' } if ($reporter -like "*$UserIdentity*") { $ticketsByRole['Requester'] += $ticket } elseif ($assignee -like "*$UserIdentity*") { $ticketsByRole['Assignee'] += $ticket } else { $ticketsByRole['Mentioned'] += $ticket } } Write-Verbose "Found $(@($jiraIssues).Count) Jira issues" } 'File' { if (-not $FilePath) { throw 'File provider requires -FilePath parameter.' } $fileTickets = @(Import-TicketExport -Path $FilePath -UserFilter $UserIdentity -UserRole $Role -MonthsBack $MonthsBack) foreach ($ticket in $fileTickets) { if ($ticket.CallerName -like "*$UserIdentity*") { $ticketsByRole['Requester'] += $ticket } elseif ($ticket.AssignedTo -like "*$UserIdentity*") { $ticketsByRole['Assignee'] += $ticket } else { $ticketsByRole['Mentioned'] += $ticket } } Write-Verbose "Imported $($fileTickets.Count) tickets from file" } } # Merge all tickets (deduplicate by number) $seenNumbers = @{} $allTickets = @() foreach ($roleKey in @('Requester', 'Assignee', 'Mentioned')) { foreach ($t in $ticketsByRole[$roleKey]) { if (-not $seenNumbers.ContainsKey($t.Number)) { $seenNumbers[$t.Number] = $true $allTickets += $t } } } $allTickets = @($allTickets | Sort-Object { try { [datetime]$_.OpenedAt } catch { [datetime]::MinValue } }) # Identify open items $openItems = @($allTickets | Where-Object { $_.State -notmatch '(?i)(closed|resolved|cancelled|completed|done)' }) # ── AI Summary ── $summary = '' $commonIssues = @() if (-not $SkipAI -and $allTickets.Count -gt 0) { Write-Verbose 'Generating AI summary...' $templatePath = Join-Path $PSScriptRoot '..\Templates\user-history-prompt.txt' if (Test-Path $templatePath) { $promptTemplate = Get-Content -Path $templatePath -Raw -Encoding UTF8 } else { $promptTemplate = @" You are an IT service management analyst. Given the following ticket history for user "{user_name}", provide a summary. Analyze tickets and provide: 1. Summary 2. As Requester patterns 3. As Assignee patterns 4. Open Items 5. Recommendations TICKET DATA: {ticket_data} "@ } $ticketDataText = ($allTickets | ForEach-Object { $roleTag = if ($_.CallerName -like "*$UserIdentity*") { 'REQUESTER' } elseif ($_.AssignedTo -like "*$UserIdentity*") { 'ASSIGNEE' } else { 'MENTIONED' } "[$roleTag] [$($_.Type)] $($_.Number) | $($_.OpenedAt) | $($_.ShortDescription) | State: $($_.State) | Priority: $($_.Priority)" }) -join "`n" $prompt = $promptTemplate -replace '\{user_name\}', $UserIdentity -replace '\{ticket_data\}', $ticketDataText $aiParams = @{ Prompt = $prompt Provider = $AIProvider } if ($AIApiKey) { $aiParams['ApiKey'] = $AIApiKey } if ($AIModel) { $aiParams['Model'] = $AIModel } if ($AIEndpoint) { $aiParams['Endpoint'] = $AIEndpoint } try { $summary = Invoke-AICompletion @aiParams # Extract common issues from AI response (look for bulleted/numbered lists) $lines = $summary -split "`n" foreach ($line in $lines) { if ($line -match '^\s*[-*]\s+(.+)$' -and $line.Length -lt 200) { $commonIssues += $Matches[1].Trim() } } # Limit to top issues $commonIssues = @($commonIssues | Select-Object -First 10) } catch { Write-Warning "AI summary generation failed: $($_.Exception.Message)" $summary = "[AI summary unavailable: $($_.Exception.Message)]" } } elseif ($allTickets.Count -eq 0) { $summary = "No tickets found for user '$UserIdentity' in the last $MonthsBack months." } # Build role breakdown as PSCustomObject for cleaner output $roleBreakdown = [PSCustomObject]@{ Requester = $ticketsByRole['Requester'] Assignee = $ticketsByRole['Assignee'] Mentioned = $ticketsByRole['Mentioned'] } # ── Build result object ── $result = [PSCustomObject]@{ UserName = $UserIdentity TotalTickets = $allTickets.Count TicketsByRole = $roleBreakdown Summary = $summary CommonIssues = $commonIssues OpenItems = $openItems RawTickets = $allTickets Provider = $Provider MonthsBack = $MonthsBack GeneratedAt = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' } # ── Generate HTML report if requested ── if ($OutputPath) { Write-Verbose "Generating HTML report: $OutputPath" New-HtmlDashboard -ReportType 'UserHistory' -Data $result -OutputPath $OutputPath Write-Host "HTML report saved to: $OutputPath" -ForegroundColor Cyan } return $result } function Convert-SNOWTicketToObject { <# .SYNOPSIS Helper to convert a raw ServiceNow ticket response into a normalized PSCustomObject. #> [CmdletBinding()] param( [Parameter(Mandatory)] [object]$Ticket, [Parameter()] [string]$UserIdentity ) # Determine ticket type from sys_class_name or table context $type = switch ($Ticket.sys_class_name) { 'incident' { 'Incident' } 'change_request' { 'Change Request' } 'problem' { 'Problem' } 'sc_request' { 'Service Request' } 'sc_req_item' { 'Requested Item' } default { 'Incident' } } [PSCustomObject]@{ Number = $Ticket.number Type = $type ShortDescription = $Ticket.short_description Description = $Ticket.description State = $Ticket.state Priority = $Ticket.priority Category = $Ticket.category Subcategory = $Ticket.subcategory OpenedAt = $Ticket.opened_at ClosedAt = $Ticket.closed_at ResolvedAt = $Ticket.resolved_at AssignedTo = if ($Ticket.assigned_to -is [string]) { $Ticket.assigned_to } else { $Ticket.assigned_to.display_value } CallerName = if ($Ticket.caller_id -is [string]) { $Ticket.caller_id } else { $Ticket.caller_id.display_value } CloseNotes = $Ticket.close_notes WorkNotes = $Ticket.work_notes CIName = '' Source = 'ServiceNow' } } |