// 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 _logger; private readonly SurveyContext _context; private readonly string _openAIDeploymentName; private bool _disposed = false; public AiAnalysisService( IConfiguration configuration, ILogger 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 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 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 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> DetectEntitiesAsync(string text) { try { if (string.IsNullOrWhiteSpace(text)) { return new List(); } 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 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 { 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 { "Unable to parse AI response" }, ProtectiveFactors = new List(), RequiresImmediateAttention = false, RecommendedAction = "Manual review recommended due to analysis error", AssessedAt = DateTime.UtcNow }; } var root = jsonDoc.RootElement; return new MentalHealthRiskAssessment { RiskLevel = Enum.TryParse(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(), ProtectiveFactors = root.TryGetProperty("protectiveFactors", out var factorsProperty) ? factorsProperty.EnumerateArray().Select(x => x.GetString() ?? "").Where(s => !string.IsNullOrEmpty(s)).ToList() : new List(), 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 { $"Analysis error: {ex.Message}" }, ProtectiveFactors = new List(), RequiresImmediateAttention = false, RecommendedAction = "Manual review recommended due to system error", AssessedAt = DateTime.UtcNow }; } } public async Task> 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 { 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(); } 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(); } var workplaceInsights = new List(); 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(); 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(); } } public async Task GenerateExecutiveSummaryAsync(List 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 { 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> 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 { 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>(content, out var err) ?? new List(); 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 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> AnalyzeQuestionResponsesAsync(int questionId, List requests) { var results = new List(); 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 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(); 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> BatchAnalyzeResponsesAsync(List requests) { var results = new List(); var tasks = new List>(); // 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> 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> 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 { 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> CompareTeamMentalHealthAsync(int questionnaireId, List teamIdentifiers) { // This would require team identification in responses // For now, return a basic implementation var result = new Dictionary(); foreach (var team in teamIdentifiers) { result[team] = await GenerateQuestionnaireOverviewAsync(questionnaireId); } return result; } public async Task> 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 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> 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(); 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 GenerateManagementDashboardAsync(int questionnaireId) { return await GenerateQuestionnaireOverviewAsync(questionnaireId); } #endregion #region Utility Methods public async Task 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 TestAzureOpenAIConnectionAsync() { try { var messages = new List { 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 ValidateAnalysisRequestAsync(AnalysisRequest request) { return Task.FromResult(!string.IsNullOrWhiteSpace(request.ResponseText) && request.ResponseId > 0 && request.QuestionId > 0); } public async Task> GetServiceHealthStatusAsync() { var status = new Dictionary(); 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)(?!))\})", 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(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) && 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(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 } }