1092 lines
42 KiB
C#
1092 lines
42 KiB
C#
// Services/Implementation/AiAnalysisService.cs
|
|
using Azure;
|
|
using Azure.AI.TextAnalytics;
|
|
using Azure.AI.OpenAI;
|
|
using OpenAI.Chat;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.Logging;
|
|
using Services.AIViewModel;
|
|
using Services.Interaces;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using System.Text.RegularExpressions;
|
|
using Data;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using System.ClientModel;
|
|
|
|
namespace Services.Implemnetation
|
|
{
|
|
public class AiAnalysisService : IAiAnalysisService, IDisposable
|
|
{
|
|
private readonly TextAnalyticsClient _textAnalyticsClient;
|
|
private readonly AzureOpenAIClient _azureOpenAIClient;
|
|
private readonly ChatClient _chatClient;
|
|
private readonly IConfiguration _configuration;
|
|
private readonly ILogger<AiAnalysisService> _logger;
|
|
private readonly SurveyContext _context;
|
|
private readonly string _openAIDeploymentName;
|
|
private bool _disposed = false;
|
|
|
|
public AiAnalysisService(
|
|
IConfiguration configuration,
|
|
ILogger<AiAnalysisService> logger,
|
|
SurveyContext context)
|
|
{
|
|
_configuration = configuration;
|
|
_logger = logger;
|
|
_context = context;
|
|
|
|
// Initialize Azure Language Service
|
|
var languageEndpoint = _configuration["AzureLanguageService:Endpoint"];
|
|
var languageKey = _configuration["AzureLanguageService:Key"];
|
|
_textAnalyticsClient = new TextAnalyticsClient(new Uri(languageEndpoint), new AzureKeyCredential(languageKey));
|
|
|
|
// Initialize Azure OpenAI
|
|
var openAIEndpoint = _configuration["AzureOpenAI:Endpoint"];
|
|
var openAIKey = _configuration["AzureOpenAI:Key"];
|
|
_openAIDeploymentName = _configuration["AzureOpenAI:DeploymentName"];
|
|
|
|
_azureOpenAIClient = new AzureOpenAIClient(new Uri(openAIEndpoint), new AzureKeyCredential(openAIKey));
|
|
_chatClient = _azureOpenAIClient.GetChatClient(_openAIDeploymentName);
|
|
|
|
_logger.LogInformation("AiAnalysisService initialized successfully");
|
|
}
|
|
|
|
#region Azure Language Service Methods
|
|
|
|
public async Task<SentimentAnalysisResult> AnalyzeSentimentAsync(string text)
|
|
{
|
|
try
|
|
{
|
|
if (string.IsNullOrWhiteSpace(text))
|
|
{
|
|
return new SentimentAnalysisResult { Sentiment = "Neutral", ConfidenceScore = 0.0 };
|
|
}
|
|
|
|
var response = await _textAnalyticsClient.AnalyzeSentimentAsync(text);
|
|
var sentiment = response.Value;
|
|
|
|
return new SentimentAnalysisResult
|
|
{
|
|
Sentiment = sentiment.Sentiment.ToString(),
|
|
ConfidenceScore = sentiment.ConfidenceScores.Positive > sentiment.ConfidenceScores.Negative
|
|
? sentiment.ConfidenceScores.Positive
|
|
: sentiment.ConfidenceScores.Negative,
|
|
PositiveScore = sentiment.ConfidenceScores.Positive,
|
|
NegativeScore = sentiment.ConfidenceScores.Negative,
|
|
NeutralScore = sentiment.ConfidenceScores.Neutral,
|
|
AnalyzedAt = DateTime.UtcNow
|
|
};
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error analyzing sentiment for text: {Text}", text);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<KeyPhrasesResult> ExtractKeyPhrasesAsync(string text)
|
|
{
|
|
try
|
|
{
|
|
if (string.IsNullOrWhiteSpace(text))
|
|
{
|
|
return new KeyPhrasesResult();
|
|
}
|
|
|
|
var response = await _textAnalyticsClient.ExtractKeyPhrasesAsync(text);
|
|
var keyPhrases = response.Value.ToList();
|
|
|
|
// Mental health specific categorization
|
|
var workplaceFactors = keyPhrases.Where(phrase =>
|
|
phrase.Contains("work") || phrase.Contains("manager") || phrase.Contains("team") ||
|
|
phrase.Contains("deadline") || phrase.Contains("pressure") || phrase.Contains("colleague") ||
|
|
phrase.Contains("office") || phrase.Contains("meeting") || phrase.Contains("project")).ToList();
|
|
|
|
var emotionalIndicators = keyPhrases.Where(phrase =>
|
|
phrase.Contains("stress") || phrase.Contains("anxious") || phrase.Contains("tired") ||
|
|
phrase.Contains("overwhelmed") || phrase.Contains("frustrated") || phrase.Contains("happy") ||
|
|
phrase.Contains("satisfied") || phrase.Contains("motivated") || phrase.Contains("burned")).ToList();
|
|
|
|
return new KeyPhrasesResult
|
|
{
|
|
KeyPhrases = keyPhrases,
|
|
WorkplaceFactors = workplaceFactors,
|
|
EmotionalIndicators = emotionalIndicators,
|
|
ExtractedAt = DateTime.UtcNow
|
|
};
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error extracting key phrases for text: {Text}", text);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<string> AnonymizeTextAsync(string text)
|
|
{
|
|
try
|
|
{
|
|
if (string.IsNullOrWhiteSpace(text))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
var response = await _textAnalyticsClient.RecognizePiiEntitiesAsync(text);
|
|
var piiEntities = response.Value;
|
|
|
|
string anonymizedText = text;
|
|
foreach (var entity in piiEntities.OrderByDescending(e => e.Offset))
|
|
{
|
|
var replacement = entity.Category.ToString() switch
|
|
{
|
|
"Person" => "[NAME]",
|
|
"Email" => "[EMAIL]",
|
|
"PhoneNumber" => "[PHONE]",
|
|
"Address" => "[ADDRESS]",
|
|
_ => "[REDACTED]"
|
|
};
|
|
|
|
anonymizedText = anonymizedText.Remove(entity.Offset, entity.Length)
|
|
.Insert(entity.Offset, replacement);
|
|
}
|
|
|
|
return anonymizedText;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error anonymizing text: {Text}", text);
|
|
return text; // Return original text if anonymization fails
|
|
}
|
|
}
|
|
|
|
public async Task<List<string>> DetectEntitiesAsync(string text)
|
|
{
|
|
try
|
|
{
|
|
if (string.IsNullOrWhiteSpace(text))
|
|
{
|
|
return new List<string>();
|
|
}
|
|
|
|
var response = await _textAnalyticsClient.RecognizeEntitiesAsync(text);
|
|
var entities = response.Value;
|
|
|
|
return entities.Select(entity => $"{entity.Category}: {entity.Text}").ToList();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error detecting entities for text: {Text}", text);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Azure OpenAI Methods
|
|
|
|
public async Task<MentalHealthRiskAssessment> AssessMentalHealthRiskAsync(string anonymizedText, string questionContext)
|
|
{
|
|
try
|
|
{
|
|
var prompt = $@"
|
|
As a mental health professional, analyze this workplace survey response and assess the mental health risk level.
|
|
|
|
Question Context: {questionContext}
|
|
Response: {anonymizedText}
|
|
|
|
Please provide:
|
|
1. Risk Level (Low, Moderate, High, Critical)
|
|
2. Risk Score (0.0 to 1.0)
|
|
3. Risk Indicators (specific concerns found)
|
|
4. Protective Factors (positive elements found)
|
|
5. Requires Immediate Attention (true/false)
|
|
6. Recommended Action
|
|
|
|
Respond in this JSON format:
|
|
{{
|
|
""riskLevel"": ""Low"",
|
|
""riskScore"": 0.0,
|
|
""riskIndicators"": [""indicator1"", ""indicator2""],
|
|
""protectiveFactors"": [""factor1"", ""factor2""],
|
|
""requiresImmediateAttention"": false,
|
|
""recommendedAction"": ""specific action recommendation""
|
|
}}";
|
|
|
|
var messages = new List<ChatMessage>
|
|
{
|
|
new SystemChatMessage("You are a mental health professional specialized in workplace wellness. Always respond with a single valid JSON object only. No markdown, no explanations."),
|
|
new UserChatMessage(prompt)
|
|
};
|
|
|
|
var chatOptions = new ChatCompletionOptions()
|
|
{
|
|
Temperature = 0.3f
|
|
};
|
|
|
|
var response = await _chatClient.CompleteChatAsync(messages, chatOptions);
|
|
var content = response.Value.Content[0].Text;
|
|
|
|
_logger.LogInformation("OpenAI Response: {Content}", content);
|
|
|
|
// Parse more defensively
|
|
var jsonDoc = ParseLenient(content, out var parseErr);
|
|
if (jsonDoc == null)
|
|
{
|
|
_logger.LogWarning("Failed to parse JSON response from OpenAI: {Err}. Raw (first 800): {Raw}",
|
|
parseErr, content?.Length > 800 ? content[..800] : content);
|
|
|
|
return new MentalHealthRiskAssessment
|
|
{
|
|
RiskLevel = RiskLevel.Moderate,
|
|
RiskScore = 0.5,
|
|
RiskIndicators = new List<string> { "Unable to parse AI response" },
|
|
ProtectiveFactors = new List<string>(),
|
|
RequiresImmediateAttention = false,
|
|
RecommendedAction = "Manual review recommended due to analysis error",
|
|
AssessedAt = DateTime.UtcNow
|
|
};
|
|
}
|
|
|
|
var root = jsonDoc.RootElement;
|
|
|
|
return new MentalHealthRiskAssessment
|
|
{
|
|
RiskLevel = Enum.TryParse<RiskLevel>(root.TryGetProperty("riskLevel", out var rl) ? (rl.GetString() ?? "Low") : "Low", out var riskLevel)
|
|
? riskLevel : RiskLevel.Low,
|
|
RiskScore = root.TryGetProperty("riskScore", out var scoreProperty)
|
|
? TryGetDoubleFlexible(scoreProperty, out var d) ? d : 0.0
|
|
: 0.0,
|
|
RiskIndicators = root.TryGetProperty("riskIndicators", out var indicatorsProperty)
|
|
? indicatorsProperty.EnumerateArray().Select(x => x.GetString() ?? "").Where(s => !string.IsNullOrEmpty(s)).ToList()
|
|
: new List<string>(),
|
|
ProtectiveFactors = root.TryGetProperty("protectiveFactors", out var factorsProperty)
|
|
? factorsProperty.EnumerateArray().Select(x => x.GetString() ?? "").Where(s => !string.IsNullOrEmpty(s)).ToList()
|
|
: new List<string>(),
|
|
RequiresImmediateAttention = root.TryGetProperty("requiresImmediateAttention", out var attentionProperty) &&
|
|
TryGetBoolFlexible(attentionProperty, out var b) && b,
|
|
RecommendedAction = root.TryGetProperty("recommendedAction", out var actionProperty) ? (actionProperty.GetString() ?? "") : "",
|
|
AssessedAt = DateTime.UtcNow
|
|
};
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error assessing mental health risk for text: {Text}", anonymizedText);
|
|
|
|
// Return a safe default assessment instead of throwing
|
|
return new MentalHealthRiskAssessment
|
|
{
|
|
RiskLevel = RiskLevel.Moderate,
|
|
RiskScore = 0.5,
|
|
RiskIndicators = new List<string> { $"Analysis error: {ex.Message}" },
|
|
ProtectiveFactors = new List<string>(),
|
|
RequiresImmediateAttention = false,
|
|
RecommendedAction = "Manual review recommended due to system error",
|
|
AssessedAt = DateTime.UtcNow
|
|
};
|
|
}
|
|
}
|
|
|
|
public async Task<List<WorkplaceInsight>> GenerateWorkplaceInsightsAsync(string anonymizedText, string questionContext)
|
|
{
|
|
try
|
|
{
|
|
var prompt = $@"
|
|
Analyze this workplace survey response and identify specific workplace insights and intervention recommendations.
|
|
|
|
Question Context: {questionContext}
|
|
Response: {anonymizedText}
|
|
|
|
Identify workplace issues and provide specific, actionable intervention recommendations. Focus on:
|
|
- Work-life balance issues
|
|
- Management/leadership concerns
|
|
- Team dynamics problems
|
|
- Workload and stress factors
|
|
- Communication issues
|
|
- Organizational culture problems
|
|
|
|
Respond in this JSON format:
|
|
{{
|
|
""insights"": [
|
|
{{
|
|
""category"": ""category name"",
|
|
""issue"": ""specific issue identified"",
|
|
""recommendedIntervention"": ""specific action to take"",
|
|
""priority"": 1,
|
|
""affectedAreas"": [""area1"", ""area2""]
|
|
}}
|
|
]
|
|
}}";
|
|
|
|
var messages = new List<ChatMessage>
|
|
{
|
|
new SystemChatMessage("You are a workplace mental health consultant. Always respond with a SINGLE valid JSON object ONLY. No markdown, no code fences, no explanations."),
|
|
new UserChatMessage(prompt)
|
|
};
|
|
|
|
var chatOptions = new ChatCompletionOptions()
|
|
{
|
|
Temperature = 0.4f
|
|
};
|
|
|
|
var response = await _chatClient.CompleteChatAsync(messages, chatOptions);
|
|
var content = response.Value.Content[0].Text;
|
|
|
|
var jsonDoc = ParseLenient(content, out var parseErr);
|
|
if (jsonDoc == null)
|
|
{
|
|
_logger.LogWarning("Insights JSON parse failed: {Err}. Raw (first 800): {Raw}",
|
|
parseErr, content?.Length > 800 ? content[..800] : content);
|
|
|
|
// Fail soft (avoid crashing controller)
|
|
return new List<WorkplaceInsight>();
|
|
}
|
|
|
|
var root = jsonDoc.RootElement;
|
|
|
|
if (!root.TryGetProperty("insights", out var insightsElement) || insightsElement.ValueKind != JsonValueKind.Array)
|
|
{
|
|
// No insights array — return empty rather than throw
|
|
return new List<WorkplaceInsight>();
|
|
}
|
|
|
|
var workplaceInsights = new List<WorkplaceInsight>();
|
|
|
|
foreach (var insight in insightsElement.EnumerateArray())
|
|
{
|
|
// Be defensive with each field
|
|
string category = insight.TryGetProperty("category", out var cat) ? (cat.GetString() ?? "") : "";
|
|
string issue = insight.TryGetProperty("issue", out var iss) ? (iss.GetString() ?? "") : "";
|
|
string recommended = insight.TryGetProperty("recommendedIntervention", out var rec) ? (rec.GetString() ?? "") : "";
|
|
int priority = 3;
|
|
if (insight.TryGetProperty("priority", out var pr))
|
|
{
|
|
if (pr.ValueKind == JsonValueKind.Number && pr.TryGetInt32(out var pInt)) priority = pInt;
|
|
else if (pr.ValueKind == JsonValueKind.String && int.TryParse(pr.GetString(), out var pStr)) priority = pStr;
|
|
}
|
|
var affectedAreas = new List<string>();
|
|
if (insight.TryGetProperty("affectedAreas", out var aa) && aa.ValueKind == JsonValueKind.Array)
|
|
{
|
|
affectedAreas = aa.EnumerateArray().Select(x => x.GetString() ?? "").Where(s => !string.IsNullOrWhiteSpace(s)).ToList();
|
|
}
|
|
|
|
workplaceInsights.Add(new WorkplaceInsight
|
|
{
|
|
Category = category,
|
|
Issue = issue,
|
|
RecommendedIntervention = recommended,
|
|
Priority = priority,
|
|
AffectedAreas = affectedAreas,
|
|
IdentifiedAt = DateTime.UtcNow
|
|
});
|
|
}
|
|
|
|
return workplaceInsights;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error generating workplace insights for text: {Text}", anonymizedText);
|
|
// Fail soft to prevent controller crash
|
|
return new List<WorkplaceInsight>();
|
|
}
|
|
}
|
|
|
|
public async Task<string> GenerateExecutiveSummaryAsync(List<ResponseAnalysisResult> analysisResults)
|
|
{
|
|
try
|
|
{
|
|
var totalResponses = analysisResults.Count;
|
|
var highRiskCount = analysisResults.Count(r => r.RiskAssessment?.RiskLevel >= RiskLevel.High);
|
|
var positiveResponses = analysisResults.Count(r => r.SentimentAnalysis?.Sentiment == "Positive");
|
|
|
|
var commonIssues = analysisResults
|
|
.SelectMany(r => r.Insights)
|
|
.GroupBy(i => i.Category)
|
|
.OrderByDescending(g => g.Count())
|
|
.Take(5)
|
|
.Select(g => $"{g.Key}: {g.Count()} instances")
|
|
.ToList();
|
|
|
|
var prompt = $@"
|
|
Create an executive summary for a workplace mental health survey analysis.
|
|
|
|
Survey Statistics:
|
|
- Total Responses: {totalResponses}
|
|
- High Risk Responses: {highRiskCount}
|
|
- Positive Responses: {positiveResponses}
|
|
- Most Common Issues: {string.Join(", ", commonIssues)}
|
|
|
|
Create a comprehensive executive summary that includes:
|
|
1. Overall mental health status
|
|
2. Key findings and trends
|
|
3. Areas of concern
|
|
4. Positive indicators
|
|
5. Immediate action items
|
|
6. Long-term recommendations
|
|
|
|
Keep it professional, actionable, and suitable for senior management.";
|
|
|
|
var messages = new List<ChatMessage>
|
|
{
|
|
new SystemChatMessage("You are an executive consultant specializing in workplace mental health reporting."),
|
|
new UserChatMessage(prompt)
|
|
};
|
|
|
|
var chatOptions = new ChatCompletionOptions()
|
|
{
|
|
Temperature = 0.3f
|
|
};
|
|
|
|
var response = await _chatClient.CompleteChatAsync(messages, chatOptions);
|
|
return response.Value.Content[0].Text;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error generating executive summary");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<List<string>> CategorizeResponseAsync(string anonymizedText)
|
|
{
|
|
try
|
|
{
|
|
var prompt = $@"
|
|
Categorize this workplace mental health survey response into relevant themes.
|
|
|
|
Response: {anonymizedText}
|
|
|
|
Choose from these categories (select all that apply):
|
|
- Work-Life Balance
|
|
- Workload Management
|
|
- Team Dynamics
|
|
- Leadership/Management
|
|
- Communication Issues
|
|
- Job Satisfaction
|
|
- Stress Management
|
|
- Career Development
|
|
- Work Environment
|
|
- Organizational Culture
|
|
- Mental Health Support
|
|
- Burnout Prevention
|
|
|
|
Respond with a JSON array of applicable categories: [""category1"", ""category2""]";
|
|
|
|
var messages = new List<ChatMessage>
|
|
{
|
|
new SystemChatMessage("You are a mental health professional categorizing workplace responses. Always respond with a JSON array only."),
|
|
new UserChatMessage(prompt)
|
|
};
|
|
|
|
var chatOptions = new ChatCompletionOptions()
|
|
{
|
|
Temperature = 0.2f
|
|
};
|
|
|
|
var response = await _chatClient.CompleteChatAsync(messages, chatOptions);
|
|
var content = response.Value.Content[0].Text;
|
|
|
|
// lenient array parse
|
|
var categories = DeserializeLenient<List<string>>(content, out var err) ?? new List<string>();
|
|
if (!string.IsNullOrEmpty(err))
|
|
{
|
|
_logger.LogWarning("Category JSON parse failed: {Err}. Raw (first 800): {Raw}",
|
|
err, content?.Length > 800 ? content[..800] : content);
|
|
}
|
|
|
|
return categories;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error categorizing response: {Text}", anonymizedText);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Combined Analysis Methods
|
|
|
|
public async Task<ResponseAnalysisResult> AnalyzeCompleteResponseAsync(AnalysisRequest request)
|
|
{
|
|
try
|
|
{
|
|
var result = new ResponseAnalysisResult
|
|
{
|
|
ResponseId = request.ResponseId,
|
|
QuestionId = request.QuestionId,
|
|
QuestionText = request.QuestionText,
|
|
ResponseText = request.ResponseText,
|
|
AnalyzedAt = DateTime.UtcNow
|
|
};
|
|
|
|
// Step 1: Anonymize the text
|
|
result.AnonymizedResponseText = await AnonymizeTextAsync(request.ResponseText);
|
|
|
|
// Step 2: Azure Language Service Analysis
|
|
if (request.IncludeSentimentAnalysis)
|
|
{
|
|
result.SentimentAnalysis = await AnalyzeSentimentAsync(result.AnonymizedResponseText);
|
|
}
|
|
|
|
if (request.IncludeKeyPhraseExtraction)
|
|
{
|
|
result.KeyPhrases = await ExtractKeyPhrasesAsync(result.AnonymizedResponseText);
|
|
}
|
|
|
|
// Step 3: Azure OpenAI Analysis
|
|
if (request.IncludeRiskAssessment)
|
|
{
|
|
result.RiskAssessment = await AssessMentalHealthRiskAsync(result.AnonymizedResponseText, request.QuestionText);
|
|
}
|
|
|
|
if (request.IncludeWorkplaceInsights)
|
|
{
|
|
result.Insights = await GenerateWorkplaceInsightsAsync(result.AnonymizedResponseText, request.QuestionText);
|
|
}
|
|
|
|
result.IsAnalysisComplete = true;
|
|
return result;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error in complete response analysis for ResponseId: {ResponseId}", request.ResponseId);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<List<ResponseAnalysisResult>> AnalyzeQuestionResponsesAsync(int questionId, List<AnalysisRequest> requests)
|
|
{
|
|
var results = new List<ResponseAnalysisResult>();
|
|
|
|
foreach (var request in requests)
|
|
{
|
|
try
|
|
{
|
|
var result = await AnalyzeCompleteResponseAsync(request);
|
|
results.Add(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error analyzing response {ResponseId} for question {QuestionId}", request.ResponseId, questionId);
|
|
// Continue with other responses even if one fails
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
public async Task<QuestionnaireAnalysisOverview> GenerateQuestionnaireOverviewAsync(int questionnaireId)
|
|
{
|
|
try
|
|
{
|
|
// Get all responses for this questionnaire
|
|
var responses = await _context.Responses
|
|
.Include(r => r.Questionnaire)
|
|
.Include(r => r.ResponseDetails)
|
|
.ThenInclude(rd => rd.Question)
|
|
.Where(r => r.QuestionnaireId == questionnaireId)
|
|
.ToListAsync();
|
|
|
|
if (!responses.Any())
|
|
{
|
|
return new QuestionnaireAnalysisOverview
|
|
{
|
|
QuestionnaireId = questionnaireId,
|
|
QuestionnaireTitle = "No responses found",
|
|
TotalResponses = 0
|
|
};
|
|
}
|
|
|
|
// Analyze text responses
|
|
var analysisResults = new List<ResponseAnalysisResult>();
|
|
foreach (var response in responses)
|
|
{
|
|
foreach (var detail in response.ResponseDetails)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(detail.TextResponse))
|
|
{
|
|
var request = new AnalysisRequest
|
|
{
|
|
ResponseId = response.Id,
|
|
QuestionId = detail.QuestionId,
|
|
ResponseText = detail.TextResponse,
|
|
QuestionText = detail.Question?.Text ?? ""
|
|
};
|
|
|
|
var analysisResult = await AnalyzeCompleteResponseAsync(request);
|
|
analysisResults.Add(analysisResult);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Generate overview
|
|
var overview = new QuestionnaireAnalysisOverview
|
|
{
|
|
QuestionnaireId = questionnaireId,
|
|
QuestionnaireTitle = responses.First().Questionnaire?.Title ?? "Unknown",
|
|
TotalResponses = responses.Count,
|
|
AnalyzedResponses = analysisResults.Count,
|
|
LastAnalyzedAt = DateTime.UtcNow
|
|
};
|
|
|
|
if (analysisResults.Any())
|
|
{
|
|
// Calculate sentiment distribution
|
|
var sentimentResults = analysisResults.Where(r => r.SentimentAnalysis != null).ToList();
|
|
if (sentimentResults.Any())
|
|
{
|
|
overview.OverallPositiveSentiment = sentimentResults.Average(r => r.SentimentAnalysis!.PositiveScore);
|
|
overview.OverallNegativeSentiment = sentimentResults.Average(r => r.SentimentAnalysis!.NegativeScore);
|
|
overview.OverallNeutralSentiment = sentimentResults.Average(r => r.SentimentAnalysis!.NeutralScore);
|
|
}
|
|
|
|
// Calculate risk distribution
|
|
var riskResults = analysisResults.Where(r => r.RiskAssessment != null).ToList();
|
|
if (riskResults.Any())
|
|
{
|
|
overview.LowRiskResponses = riskResults.Count(r => r.RiskAssessment!.RiskLevel == RiskLevel.Low);
|
|
overview.ModerateRiskResponses = riskResults.Count(r => r.RiskAssessment!.RiskLevel == RiskLevel.Moderate);
|
|
overview.HighRiskResponses = riskResults.Count(r => r.RiskAssessment!.RiskLevel == RiskLevel.High);
|
|
overview.CriticalRiskResponses = riskResults.Count(r => r.RiskAssessment!.RiskLevel == RiskLevel.Critical);
|
|
}
|
|
|
|
// Get top workplace issues
|
|
overview.TopWorkplaceIssues = analysisResults
|
|
.SelectMany(r => r.Insights)
|
|
.GroupBy(i => i.Category)
|
|
.OrderByDescending(g => g.Count())
|
|
.Take(5)
|
|
.Select(g => g.First())
|
|
.ToList();
|
|
|
|
// Get common key phrases
|
|
overview.MostCommonKeyPhrases = analysisResults
|
|
.Where(r => r.KeyPhrases != null)
|
|
.SelectMany(r => r.KeyPhrases!.KeyPhrases)
|
|
.GroupBy(k => k)
|
|
.OrderByDescending(g => g.Count())
|
|
.Take(10)
|
|
.Select(g => g.Key)
|
|
.ToList();
|
|
|
|
// Generate executive summary
|
|
overview.ExecutiveSummary = await GenerateExecutiveSummaryAsync(analysisResults);
|
|
}
|
|
|
|
return overview;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error generating questionnaire overview for QuestionnaireId: {QuestionnaireId}", questionnaireId);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<List<ResponseAnalysisResult>> BatchAnalyzeResponsesAsync(List<AnalysisRequest> requests)
|
|
{
|
|
var results = new List<ResponseAnalysisResult>();
|
|
var tasks = new List<Task<ResponseAnalysisResult>>();
|
|
|
|
// Process in batches to avoid overwhelming the API
|
|
const int batchSize = 5;
|
|
for (int i = 0; i < requests.Count; i += batchSize)
|
|
{
|
|
var batch = requests.Skip(i).Take(batchSize);
|
|
foreach (var request in batch)
|
|
{
|
|
tasks.Add(AnalyzeCompleteResponseAsync(request));
|
|
}
|
|
|
|
// Wait for current batch to complete before starting next
|
|
var batchResults = await Task.WhenAll(tasks);
|
|
results.AddRange(batchResults);
|
|
tasks.Clear();
|
|
|
|
// Small delay between batches to respect API limits
|
|
await Task.Delay(1000);
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Mental Health Specific Methods
|
|
|
|
public async Task<List<ResponseAnalysisResult>> IdentifyHighRiskResponsesAsync(int questionnaireId)
|
|
{
|
|
try
|
|
{
|
|
var overview = await GenerateQuestionnaireOverviewAsync(questionnaireId);
|
|
var allResults = await ExportAnonymizedAnalysisAsync(questionnaireId);
|
|
|
|
return allResults
|
|
.Where(r => r.RiskAssessment != null &&
|
|
(r.RiskAssessment.RiskLevel >= RiskLevel.High || r.RiskAssessment.RequiresImmediateAttention))
|
|
.OrderByDescending(r => r.RiskAssessment!.RiskScore)
|
|
.ToList();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error identifying high risk responses for QuestionnaireId: {QuestionnaireId}", questionnaireId);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<List<WorkplaceInsight>> AnalyzeMentalHealthTrendsAsync(int questionnaireId, DateTime fromDate, DateTime toDate)
|
|
{
|
|
try
|
|
{
|
|
var responses = await _context.Responses
|
|
.Include(r => r.ResponseDetails)
|
|
.ThenInclude(rd => rd.Question)
|
|
.Where(r => r.QuestionnaireId == questionnaireId &&
|
|
r.SubmissionDate >= fromDate &&
|
|
r.SubmissionDate <= toDate)
|
|
.OrderBy(r => r.SubmissionDate)
|
|
.ToListAsync();
|
|
|
|
// This would typically involve more complex trend analysis
|
|
// For now, return general insights
|
|
var insights = new List<WorkplaceInsight>
|
|
{
|
|
new WorkplaceInsight
|
|
{
|
|
Category = "Trend Analysis",
|
|
Issue = $"Analysis of {responses.Count} responses from {fromDate:yyyy-MM-dd} to {toDate:yyyy-MM-dd}",
|
|
RecommendedIntervention = "Regular monitoring and follow-up assessments recommended",
|
|
Priority = 3,
|
|
IdentifiedAt = DateTime.UtcNow
|
|
}
|
|
};
|
|
|
|
return insights;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error analyzing mental health trends for QuestionnaireId: {QuestionnaireId}", questionnaireId);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<Dictionary<string, QuestionnaireAnalysisOverview>> CompareTeamMentalHealthAsync(int questionnaireId, List<string> teamIdentifiers)
|
|
{
|
|
// This would require team identification in responses
|
|
// For now, return a basic implementation
|
|
var result = new Dictionary<string, QuestionnaireAnalysisOverview>();
|
|
|
|
foreach (var team in teamIdentifiers)
|
|
{
|
|
result[team] = await GenerateQuestionnaireOverviewAsync(questionnaireId);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public async Task<List<WorkplaceInsight>> GenerateInterventionRecommendationsAsync(int questionnaireId)
|
|
{
|
|
try
|
|
{
|
|
var overview = await GenerateQuestionnaireOverviewAsync(questionnaireId);
|
|
return overview.TopWorkplaceIssues;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error generating intervention recommendations for QuestionnaireId: {QuestionnaireId}", questionnaireId);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Reporting Methods
|
|
|
|
public async Task<string> GenerateDetailedAnalysisReportAsync(int questionnaireId)
|
|
{
|
|
try
|
|
{
|
|
var overview = await GenerateQuestionnaireOverviewAsync(questionnaireId);
|
|
var highRiskResponses = await IdentifyHighRiskResponsesAsync(questionnaireId);
|
|
|
|
var report = new StringBuilder();
|
|
report.AppendLine($"# Mental Health Analysis Report");
|
|
report.AppendLine($"**Questionnaire:** {overview.QuestionnaireTitle}");
|
|
report.AppendLine($"**Analysis Date:** {DateTime.Now:yyyy-MM-dd HH:mm}");
|
|
report.AppendLine($"**Total Responses:** {overview.TotalResponses}");
|
|
report.AppendLine($"**Analyzed Responses:** {overview.AnalyzedResponses}");
|
|
report.AppendLine();
|
|
|
|
report.AppendLine("## Executive Summary");
|
|
report.AppendLine(overview.ExecutiveSummary);
|
|
report.AppendLine();
|
|
|
|
report.AppendLine("## Risk Distribution");
|
|
report.AppendLine($"- Low Risk: {overview.LowRiskResponses}");
|
|
report.AppendLine($"- Moderate Risk: {overview.ModerateRiskResponses}");
|
|
report.AppendLine($"- High Risk: {overview.HighRiskResponses}");
|
|
report.AppendLine($"- Critical Risk: {overview.CriticalRiskResponses}");
|
|
report.AppendLine();
|
|
|
|
if (highRiskResponses.Any())
|
|
{
|
|
report.AppendLine("## High Risk Responses Requiring Attention");
|
|
report.AppendLine($"Found {highRiskResponses.Count} responses requiring immediate attention.");
|
|
report.AppendLine();
|
|
}
|
|
|
|
report.AppendLine("## Top Workplace Issues");
|
|
foreach (var issue in overview.TopWorkplaceIssues.Take(5))
|
|
{
|
|
report.AppendLine($"- **{issue.Category}:** {issue.Issue}");
|
|
report.AppendLine($" - Recommended Action: {issue.RecommendedIntervention}");
|
|
}
|
|
|
|
return report.ToString();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error generating detailed analysis report for QuestionnaireId: {QuestionnaireId}", questionnaireId);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<List<ResponseAnalysisResult>> ExportAnonymizedAnalysisAsync(int questionnaireId)
|
|
{
|
|
try
|
|
{
|
|
var responses = await _context.Responses
|
|
.Include(r => r.ResponseDetails)
|
|
.ThenInclude(rd => rd.Question)
|
|
.Where(r => r.QuestionnaireId == questionnaireId)
|
|
.ToListAsync();
|
|
|
|
var results = new List<ResponseAnalysisResult>();
|
|
|
|
foreach (var response in responses)
|
|
{
|
|
foreach (var detail in response.ResponseDetails)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(detail.TextResponse))
|
|
{
|
|
var request = new AnalysisRequest
|
|
{
|
|
ResponseId = response.Id,
|
|
QuestionId = detail.QuestionId,
|
|
ResponseText = detail.TextResponse,
|
|
QuestionText = detail.Question?.Text ?? ""
|
|
};
|
|
|
|
var result = await AnalyzeCompleteResponseAsync(request);
|
|
results.Add(result);
|
|
}
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error exporting anonymized analysis for QuestionnaireId: {QuestionnaireId}", questionnaireId);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<QuestionnaireAnalysisOverview> GenerateManagementDashboardAsync(int questionnaireId)
|
|
{
|
|
return await GenerateQuestionnaireOverviewAsync(questionnaireId);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Utility Methods
|
|
|
|
public async Task<bool> TestAzureLanguageServiceConnectionAsync()
|
|
{
|
|
try
|
|
{
|
|
await _textAnalyticsClient.AnalyzeSentimentAsync("Test connection");
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Azure Language Service connection test failed");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public async Task<bool> TestAzureOpenAIConnectionAsync()
|
|
{
|
|
try
|
|
{
|
|
var messages = new List<ChatMessage>
|
|
{
|
|
new UserChatMessage("Test connection")
|
|
};
|
|
|
|
var chatOptions = new ChatCompletionOptions();
|
|
|
|
await _chatClient.CompleteChatAsync(messages, chatOptions);
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Azure OpenAI connection test failed");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public Task<bool> ValidateAnalysisRequestAsync(AnalysisRequest request)
|
|
{
|
|
return Task.FromResult(!string.IsNullOrWhiteSpace(request.ResponseText) &&
|
|
request.ResponseId > 0 &&
|
|
request.QuestionId > 0);
|
|
}
|
|
|
|
public async Task<Dictionary<string, bool>> GetServiceHealthStatusAsync()
|
|
{
|
|
var status = new Dictionary<string, bool>();
|
|
|
|
status["AzureLanguageService"] = await TestAzureLanguageServiceConnectionAsync();
|
|
status["AzureOpenAI"] = await TestAzureOpenAIConnectionAsync();
|
|
|
|
return status;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IDisposable
|
|
|
|
public void Dispose()
|
|
{
|
|
Dispose(true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
protected virtual void Dispose(bool disposing)
|
|
{
|
|
if (!_disposed)
|
|
{
|
|
if (disposing)
|
|
{
|
|
// Dispose managed resources
|
|
_context?.Dispose();
|
|
}
|
|
_disposed = true;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Private JSON Helpers (no new models)
|
|
|
|
private static readonly Regex JsonObjectRegex =
|
|
new(@"(\{(?:[^{}]|(?<o>\{)|(?<-o>\}))*(?(o)(?!))\})",
|
|
RegexOptions.Singleline | RegexOptions.Compiled);
|
|
|
|
private static bool TryExtractJsonObject(string text, out string json)
|
|
{
|
|
json = text?.Trim() ?? "";
|
|
|
|
// strip markdown code fences if present
|
|
if (json.StartsWith("```"))
|
|
{
|
|
json = Regex.Replace(json, "^```(?:json)?\\s*|\\s*```$", "",
|
|
RegexOptions.IgnoreCase | RegexOptions.Singleline).Trim();
|
|
}
|
|
|
|
if (json.StartsWith("{") && json.EndsWith("}"))
|
|
return true;
|
|
|
|
var m = JsonObjectRegex.Match(json);
|
|
if (!m.Success) return false;
|
|
|
|
json = m.Value;
|
|
return true;
|
|
}
|
|
|
|
private static JsonDocument? ParseLenient(string content, out string? error)
|
|
{
|
|
error = null;
|
|
|
|
if (!TryExtractJsonObject(content, out var jsonOnly))
|
|
{
|
|
error = "No JSON object found in model response.";
|
|
return null;
|
|
}
|
|
|
|
var docOptions = new JsonDocumentOptions
|
|
{
|
|
AllowTrailingCommas = true,
|
|
CommentHandling = JsonCommentHandling.Skip
|
|
};
|
|
|
|
try
|
|
{
|
|
return JsonDocument.Parse(jsonOnly, docOptions);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
error = ex.Message;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static T? DeserializeLenient<T>(string content, out string? error)
|
|
{
|
|
error = null;
|
|
|
|
if (!TryExtractJsonObject(content, out var jsonOnly))
|
|
{
|
|
// also try plain array (for CategorizeResponseAsync)
|
|
var trimmed = (content ?? "").Trim();
|
|
if (typeof(T) == typeof(List<string>) && trimmed.StartsWith("[") && trimmed.EndsWith("]"))
|
|
{
|
|
jsonOnly = trimmed;
|
|
}
|
|
else
|
|
{
|
|
error = "No JSON object/array found in model response.";
|
|
return default;
|
|
}
|
|
}
|
|
|
|
var options = new JsonSerializerOptions
|
|
{
|
|
PropertyNameCaseInsensitive = true,
|
|
AllowTrailingCommas = true,
|
|
NumberHandling = JsonNumberHandling.AllowReadingFromString
|
|
};
|
|
|
|
try
|
|
{
|
|
return JsonSerializer.Deserialize<T>(jsonOnly, options);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
error = ex.Message;
|
|
return default;
|
|
}
|
|
}
|
|
|
|
private static bool TryGetDoubleFlexible(JsonElement el, out double value)
|
|
{
|
|
value = 0;
|
|
if (el.ValueKind == JsonValueKind.Number) return el.TryGetDouble(out value);
|
|
if (el.ValueKind == JsonValueKind.String) return double.TryParse(el.GetString(), out value);
|
|
return false;
|
|
}
|
|
|
|
private static bool TryGetBoolFlexible(JsonElement el, out bool value)
|
|
{
|
|
value = false;
|
|
if (el.ValueKind == JsonValueKind.True) { value = true; return true; }
|
|
if (el.ValueKind == JsonValueKind.False) { value = false; return true; }
|
|
if (el.ValueKind == JsonValueKind.String) return bool.TryParse(el.GetString(), out value);
|
|
return false;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|