using Azure.AI.OpenAI; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using OptimAI.BRE.Shared.Domain; using System.Text.Json; namespace OptimAI.BRE.AIEngine.Application; public sealed class AiCreditAnalystService : IAiCreditAnalystService { private readonly OpenAIClient _openAiClient; private readonly AiOptions _options; private readonly IAiPromptRepository _promptRepo; private readonly ILogger _logger; public AiCreditAnalystService( OpenAIClient openAiClient, IOptions options, IAiPromptRepository promptRepo, ILogger logger) { _openAiClient = openAiClient; _options = options.Value; _promptRepo = promptRepo; _logger = logger; } public async Task AnalyzeCreditAsync(CreditAnalysisRequest request, CancellationToken ct = default) { var systemPrompt = await _promptRepo.GetPromptAsync("CREDIT_ANALYSIS_SYSTEM") ?? GetDefaultCreditAnalysisSystemPrompt(); var userPrompt = BuildCreditAnalysisPrompt(request); try { var chatOptions = new ChatCompletionsOptions { DeploymentName = _options.ModelName, Temperature = 0.3f, MaxTokens = 2500, Messages = { new ChatRequestSystemMessage(systemPrompt), new ChatRequestUserMessage(userPrompt) }, ResponseFormat = ChatCompletionsResponseFormat.JsonObject }; var response = await _openAiClient.GetChatCompletionsAsync(chatOptions, ct); var content = response.Value.Choices[0].Message.Content; var analysis = JsonSerializer.Deserialize(content, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return MapToAiAnalysis(analysis!); } catch (Exception ex) { _logger.LogError(ex, "AI credit analysis failed"); return GetFallbackAnalysis(request); } } public async Task GenerateRuleFromPromptAsync(RuleGenerationRequest request, CancellationToken ct = default) { var systemPrompt = await _promptRepo.GetPromptAsync("RULE_GENERATION_SYSTEM") ?? GetRuleGenerationSystemPrompt(); var fieldCatalogJson = JsonSerializer.Serialize(request.AvailableFields?.Take(50)); var userPrompt = $""" Generate a BRE rule definition from this natural language description: User Input: {request.UserPrompt} Available Fields: {fieldCatalogJson} Product Type: {request.ProductType ?? "GENERAL"} Return a JSON object with this exact structure: {{ "ruleName": "string", "ruleCode": "string (snake_case)", "ruleType": "string (ELIGIBILITY|CREDIT|BUREAU|FI|VALUATION|FRAUD|COMPLIANCE)", "description": "string", "conditions": {{ "operator": "AND|OR", "rules": [ {{ "id": "uuid", "isGroup": false, "field": "field.path", "operator": "LESS_THAN|GREATER_THAN|EQUALS|etc", "value": "value", "valueType": "Literal" }} ] }}, "actions": [ {{ "id": "uuid", "type": "SetDecision|SetRisk|AddDeviation|SetTrafficLight", "value": "string" }} ] }} """; try { var chatOptions = new ChatCompletionsOptions { DeploymentName = _options.ModelName, Temperature = 0.2f, MaxTokens = 2000, Messages = { new ChatRequestSystemMessage(systemPrompt), new ChatRequestUserMessage(userPrompt) }, ResponseFormat = ChatCompletionsResponseFormat.JsonObject }; var response = await _openAiClient.GetChatCompletionsAsync(chatOptions, ct); var content = response.Value.Choices[0].Message.Content; return JsonSerializer.Deserialize(content, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); } catch (Exception ex) { _logger.LogError(ex, "AI rule generation failed for prompt: {Prompt}", request.UserPrompt); return null; } } public async Task AnalyzeDeviationsAsync(DeviationAnalysisRequest request, CancellationToken ct = default) { var prompt = $""" You are a credit risk expert analyzing loan application deviations. Application Data: {JsonSerializer.Serialize(request.ApplicationData, new JsonSerializerOptions { WriteIndented = true })} Detected Deviations: {JsonSerializer.Serialize(request.Deviations, new JsonSerializerOptions { WriteIndented = true })} Analyze these deviations and provide: 1. Combined risk impact 2. Whether deviations can be mitigated 3. Recommended approval conditions 4. Documents needed to override each deviation Respond in JSON format with fields: riskImpact, canMitigate, approvalConditions (array), documentRequirements (array), overallRecommendation. """; try { var chatOptions = new ChatCompletionsOptions { DeploymentName = _options.ModelName, Temperature = 0.3f, MaxTokens = 1500, Messages = { new ChatRequestUserMessage(prompt) }, ResponseFormat = ChatCompletionsResponseFormat.JsonObject }; var response = await _openAiClient.GetChatCompletionsAsync(chatOptions, ct); var content = response.Value.Choices[0].Message.Content; return JsonSerializer.Deserialize(content, new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!; } catch (Exception ex) { _logger.LogError(ex, "Deviation analysis failed"); return new DeviationAnalysis { RiskImpact = "Unable to analyze - manual review required", CanMitigate = false, OverallRecommendation = "Manual underwriter review required" }; } } private static string BuildCreditAnalysisPrompt(CreditAnalysisRequest request) { var deviationList = request.Deviations.Any() ? string.Join("\n", request.Deviations.Select(d => $"- {d.DeviationName} ({d.Severity}): {d.Reason}")) : "None"; return $""" Analyze this loan application and provide a comprehensive credit assessment: === APPLICANT PROFILE === Age: {GetField(request.Data, "applicant.age")} Employment Type: {GetField(request.Data, "employment.type")} Monthly Income: ₹{GetField(request.Data, "employment.monthly_income")} === BUREAU PROFILE === CIBIL Score: {GetField(request.Data, "bureau.cibil_score")} Max DPD (24M): {GetField(request.Data, "bureau.max_dpd_24m")} Total Active Loans: {GetField(request.Data, "bureau.total_active_loans")} Total EMI Obligation: ₹{GetField(request.Data, "bureau.total_emi_obligation")} Written Off: {GetField(request.Data, "bureau.written_off_amount")} === LOAN DETAILS === Loan Amount: ₹{GetField(request.Data, "loan.amount")} Product: {request.ProductCode} FOIR: {GetField(request.Data, "ratios.foir")}% LTV: {GetField(request.Data, "ratios.ltv")}% === BRE DECISION === Final Decision: {request.Decision} Risk Score: {request.RiskScore}/100 Risk Category: {request.RiskCategory} === DEVIATIONS === {deviationList} Provide response in JSON with these exact fields: riskSummary, creditSummary, strengths (array), weaknesses (array), deviationsSummary, approvalRecommendation, rejectionReasons (array), additionalDocuments (array), underwritingNotes, confidenceScore (0-1) """; } private static string? GetField(Dictionary data, string path) { var parts = path.Split('.'); object? current = data; foreach (var part in parts) { if (current is Dictionary dict) { current = dict.TryGetValue(part, out var val) ? val : null; } else return null; } return current?.ToString(); } private static AiAnalysis MapToAiAnalysis(AiAnalysisJson json) => new() { RiskSummary = json.RiskSummary ?? "Risk analysis not available", CreditSummary = json.CreditSummary ?? "Credit summary not available", Strengths = json.Strengths ?? new(), Weaknesses = json.Weaknesses ?? new(), DeviationsSummary = json.DeviationsSummary ?? "", ApprovalRecommendation = json.ApprovalRecommendation ?? "", RejectionReasons = json.RejectionReasons ?? new(), AdditionalDocuments = json.AdditionalDocuments ?? new(), UnderwritingNotes = json.UnderwritingNotes ?? "", ConfidenceScore = json.ConfidenceScore }; private static AiAnalysis GetFallbackAnalysis(CreditAnalysisRequest request) => new() { RiskSummary = $"Risk Score: {request.RiskScore}/100 - {request.RiskCategory} Risk", CreditSummary = $"Application decision: {request.Decision}. Manual review recommended.", Strengths = new List { "Application data available for review" }, Weaknesses = request.Deviations.Select(d => d.DeviationName).ToList(), DeviationsSummary = $"{request.Deviations.Count} deviation(s) detected", ApprovalRecommendation = request.Decision == "APPROVE" ? "Recommend approval" : "Further review required", RejectionReasons = new(), AdditionalDocuments = new(), UnderwritingNotes = "AI analysis unavailable - manual underwriter assessment required", ConfidenceScore = 0 }; private static string GetDefaultCreditAnalysisSystemPrompt() => """ You are OPTIM AI, an expert credit risk analyst with 20+ years experience in banking, NBFCs, and lending. You specialize in vehicle finance, tractor loans, MSME lending, and consumer credit. Your role: Analyze loan applications and provide objective, data-driven credit assessments. Guidelines: - Be concise but comprehensive - Use Indian banking terminology and regulations - Reference RBI guidelines where applicable - Identify genuine strengths, not just positives - Be specific about weaknesses and their impact - Recommend specific documents to mitigate risks - Underwriting notes should be actionable for credit managers Always respond in valid JSON format only. """; private static string GetRuleGenerationSystemPrompt() => """ You are OPTIM AI Rule Generator. You convert natural language credit policy descriptions into structured BRE rule definitions. Field naming conventions: - Bureau fields: bureau.cibil_score, bureau.max_dpd_24m, bureau.total_active_loans, bureau.written_off_amount - Income fields: employment.monthly_income, employment.annual_income, employment.vintage_months - Ratio fields: ratios.foir, ratios.ltv, ratios.dscr - Applicant fields: applicant.age, applicant.gender, applicant.pan_number - Vehicle fields: vehicle.age_years, vehicle.valuation, vehicle.type - FI fields: fi.verified, fi.negative, fi.address_match - Loan fields: loan.amount, loan.tenure_months, loan.emi Operator values: EQUALS, NOT_EQUALS, GREATER_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN, LESS_THAN_OR_EQUAL, BETWEEN, IN, NOT_IN, IS_NULL, IS_NOT_NULL, IS_TRUE, IS_FALSE Action types: SetDecision (APPROVE/REJECT/DEVIATION/REFER), SetRisk (LOW/MEDIUM/HIGH/CRITICAL), AddDeviation (deviation code), SetTrafficLight (GREEN/AMBER/RED) Always generate valid, executable rule definitions. Use snake_case for ruleCode. Respond with valid JSON only. """; } // ============================================================ // SUPPORTING TYPES // ============================================================ public interface IAiCreditAnalystService { Task AnalyzeCreditAsync(CreditAnalysisRequest request, CancellationToken ct = default); Task GenerateRuleFromPromptAsync(RuleGenerationRequest request, CancellationToken ct = default); Task AnalyzeDeviationsAsync(DeviationAnalysisRequest request, CancellationToken ct = default); } public class CreditAnalysisRequest { public Dictionary Data { get; set; } = new(); public string Decision { get; set; } = default!; public decimal RiskScore { get; set; } public string RiskCategory { get; set; } = default!; public string? ProductCode { get; set; } public List Deviations { get; set; } = new(); } public class RuleGenerationRequest { public string UserPrompt { get; set; } = default!; public string? ProductType { get; set; } public List? AvailableFields { get; set; } public Guid TenantId { get; set; } public Guid CreatedBy { get; set; } } public class DeviationAnalysisRequest { public Dictionary ApplicationData { get; set; } = new(); public List Deviations { get; set; } = new(); } public class DeviationAnalysis { public string RiskImpact { get; set; } = default!; public bool CanMitigate { get; set; } public List ApprovalConditions { get; set; } = new(); public List DocumentRequirements { get; set; } = new(); public string OverallRecommendation { get; set; } = default!; } public class AiOptions { public string Endpoint { get; set; } = default!; public string ApiKey { get; set; } = default!; public string ModelName { get; set; } = "gpt-4o"; public bool UseAzureOpenAI { get; set; } = true; } public interface IAiPromptRepository { Task GetPromptAsync(string promptCode, CancellationToken ct = default); } private class AiAnalysisJson { public string? RiskSummary { get; set; } public string? CreditSummary { get; set; } public List? Strengths { get; set; } public List? Weaknesses { get; set; } public string? DeviationsSummary { get; set; } public string? ApprovalRecommendation { get; set; } public List? RejectionReasons { get; set; } public List? AdditionalDocuments { get; set; } public string? UnderwritingNotes { get; set; } public double ConfidenceScore { get; set; } }