From b63a8fe94078ec0e10cf0b273b70dedaafcd341f Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Fri, 6 Mar 2026 12:08:51 -0800 Subject: [PATCH 01/58] feature/AB#32216-BuildAIBase --- .../Unity.GrantManager/Unity.GrantManager.sln | 31 + .../modules/Unity.AI/.gitattributes | 1 + .../modules/Unity.AI/.gitignore | 262 ++ .../modules/Unity.AI/.prettierrc | 5 + .../modules/Unity.AI/NuGet.Config | 5 + .../modules/Unity.AI/common.props | 24 + .../AIApplicationContractsModule.cs | 15 + .../AIRemoteServiceConsts.cs | 7 + .../FodyWeavers.xml | 3 + .../FodyWeavers.xsd | 30 + .../AIPermissionDefinitionProvider.cs | 20 + .../Permissions/AIPermissions.cs | 11 + .../Unity.AI.Application.Contracts.csproj | 25 + .../src/Unity.AI.Application/AIAppService.cs | 11 + .../AIApplicationAutoMapperProfile.cs | 11 + .../AIApplicationModule.cs | 56 + .../Domain/AIDbProperties.cs | 16 + .../EntityFrameworkCore/AIDbContext.cs | 17 + .../AIDbContextModelCreatingExtensions.cs | 20 + .../AIEntityFrameworkCoreModule.cs | 15 + .../EntityFrameworkCore/IAIDbContext.cs | 11 + .../src/Unity.AI.Application/FodyWeavers.xml | 3 + .../src/Unity.AI.Application/FodyWeavers.xsd | 30 + .../Unity.AI.Application.csproj | 31 + .../Unity.AI.Domain.Shared/AIErrorCodes.cs | 7 + .../Unity.AI.Domain.Shared/AISharedModule.cs | 40 + .../Unity.AI.Domain.Shared/FodyWeavers.xml | 3 + .../Unity.AI.Domain.Shared/FodyWeavers.xsd | 30 + .../Localization/AI/en.json | 7 + .../Localization/AIResource.cs | 9 + .../Unity.AI.Shared.csproj | 34 + .../Unity.AI.Web/AIWebAutoMapperProfile.cs | 11 + .../Unity.AI/src/Unity.AI.Web/AIWebModule.cs | 44 + .../Unity.AI/src/Unity.AI.Web/FodyWeavers.xml | 3 + .../Unity.AI/src/Unity.AI.Web/FodyWeavers.xsd | 30 + .../Unity.AI.Web/Pages/_ViewImports.cshtml | 6 + .../Properties/launchSettings.json | 12 + .../src/Unity.AI.Web/Unity.AI.Web.csproj | 42 + .../GrantTenantDbContext.cs | 2 + .../20260306195601_AISchema.Designer.cs | 2694 +++++++++++++++++ .../HostMigrations/20260306195601_AISchema.cs | 21 + .../GrantManagerDbContextModelSnapshot.cs | 101 +- ...ty.GrantManager.EntityFrameworkCore.csproj | 1 + .../Unity.GrantManager.Web.csproj | 1 + 44 files changed, 3742 insertions(+), 16 deletions(-) create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/.gitattributes create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/.gitignore create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/.prettierrc create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/NuGet.Config create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/common.props create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AIApplicationContractsModule.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AIRemoteServiceConsts.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/FodyWeavers.xml create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/FodyWeavers.xsd create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissionDefinitionProvider.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissions.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Unity.AI.Application.Contracts.csproj create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AIAppService.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AIApplicationAutoMapperProfile.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AIApplicationModule.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Domain/AIDbProperties.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIDbContext.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIDbContextModelCreatingExtensions.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIEntityFrameworkCoreModule.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/IAIDbContext.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/FodyWeavers.xml create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/FodyWeavers.xsd create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Unity.AI.Application.csproj create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/AIErrorCodes.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/AISharedModule.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/FodyWeavers.xml create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/FodyWeavers.xsd create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AI/en.json create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AIResource.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Unity.AI.Shared.csproj create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/AIWebAutoMapperProfile.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/AIWebModule.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/FodyWeavers.xml create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/FodyWeavers.xsd create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/_ViewImports.cshtml create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Properties/launchSettings.json create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Unity.AI.Web.csproj create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260306195601_AISchema.Designer.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260306195601_AISchema.cs diff --git a/applications/Unity.GrantManager/Unity.GrantManager.sln b/applications/Unity.GrantManager/Unity.GrantManager.sln index 567d2f71c0..72ef37a99f 100644 --- a/applications/Unity.GrantManager/Unity.GrantManager.sln +++ b/applications/Unity.GrantManager/Unity.GrantManager.sln @@ -155,6 +155,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unity.Flex.Web.Tests", "mod EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.Reporting.Web", "modules\Unity.Reporting\src\Unity.Reporting.Web\Unity.Reporting.Web.csproj", "{3E4E5506-9820-4650-8062-4A07FB2C851A}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Unity.AI", "Unity.AI", "{BA2040C4-DC9D-44D2-B8A8-5A18D3D649AB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unity.AI.Shared", "modules\Unity.AI\src\Unity.AI.Domain.Shared\Unity.AI.Shared.csproj", "{7C0E6C61-0903-4D48-B9ED-FFE08C4D420A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unity.AI.Application.Contracts", "modules\Unity.AI\src\Unity.AI.Application.Contracts\Unity.AI.Application.Contracts.csproj", "{3ACF64C1-492A-4BE6-B270-0F755C65F30B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unity.AI.Application", "modules\Unity.AI\src\Unity.AI.Application\Unity.AI.Application.csproj", "{7CF9D364-2018-4199-879B-371F6E1AC58B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.AI.Web", "modules\Unity.AI\src\Unity.AI.Web\Unity.AI.Web.csproj", "{378A4EB8-3DC1-420E-98B5-798DE71BEF0D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -397,6 +407,22 @@ Global {3E4E5506-9820-4650-8062-4A07FB2C851A}.Debug|Any CPU.Build.0 = Debug|Any CPU {3E4E5506-9820-4650-8062-4A07FB2C851A}.Release|Any CPU.ActiveCfg = Release|Any CPU {3E4E5506-9820-4650-8062-4A07FB2C851A}.Release|Any CPU.Build.0 = Release|Any CPU + {7C0E6C61-0903-4D48-B9ED-FFE08C4D420A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C0E6C61-0903-4D48-B9ED-FFE08C4D420A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C0E6C61-0903-4D48-B9ED-FFE08C4D420A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C0E6C61-0903-4D48-B9ED-FFE08C4D420A}.Release|Any CPU.Build.0 = Release|Any CPU + {3ACF64C1-492A-4BE6-B270-0F755C65F30B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3ACF64C1-492A-4BE6-B270-0F755C65F30B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3ACF64C1-492A-4BE6-B270-0F755C65F30B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3ACF64C1-492A-4BE6-B270-0F755C65F30B}.Release|Any CPU.Build.0 = Release|Any CPU + {7CF9D364-2018-4199-879B-371F6E1AC58B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7CF9D364-2018-4199-879B-371F6E1AC58B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7CF9D364-2018-4199-879B-371F6E1AC58B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7CF9D364-2018-4199-879B-371F6E1AC58B}.Release|Any CPU.Build.0 = Release|Any CPU + {378A4EB8-3DC1-420E-98B5-798DE71BEF0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {378A4EB8-3DC1-420E-98B5-798DE71BEF0D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {378A4EB8-3DC1-420E-98B5-798DE71BEF0D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {378A4EB8-3DC1-420E-98B5-798DE71BEF0D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -475,6 +501,11 @@ Global {0355D299-4880-4F11-84A9-E14639A76AC4} = {DC64FA90-4E98-442F-BBA9-116940A928CF} {5F4CFB7E-A14A-40A1-8833-A55CB296D31B} = {CDE485CC-D6EA-457A-88D6-DEEAF7CAC424} {3E4E5506-9820-4650-8062-4A07FB2C851A} = {FF8024E0-68D2-4716-8812-E6D16730F4CC} + {BA2040C4-DC9D-44D2-B8A8-5A18D3D649AB} = {00099710-CF66-4BD2-932C-5B7534B78185} + {7C0E6C61-0903-4D48-B9ED-FFE08C4D420A} = {BA2040C4-DC9D-44D2-B8A8-5A18D3D649AB} + {3ACF64C1-492A-4BE6-B270-0F755C65F30B} = {BA2040C4-DC9D-44D2-B8A8-5A18D3D649AB} + {7CF9D364-2018-4199-879B-371F6E1AC58B} = {BA2040C4-DC9D-44D2-B8A8-5A18D3D649AB} + {378A4EB8-3DC1-420E-98B5-798DE71BEF0D} = {BA2040C4-DC9D-44D2-B8A8-5A18D3D649AB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {28315BFD-90E7-4E14-A2EA-F3D23AF4126F} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/.gitattributes b/applications/Unity.GrantManager/modules/Unity.AI/.gitattributes new file mode 100644 index 0000000000..c941e52669 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/.gitattributes @@ -0,0 +1 @@ +**/wwwroot/libs/** linguist-vendored diff --git a/applications/Unity.GrantManager/modules/Unity.AI/.gitignore b/applications/Unity.GrantManager/modules/Unity.AI/.gitignore new file mode 100644 index 0000000000..e278eb5d95 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/.gitignore @@ -0,0 +1,262 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# Reporting +host/Unity.Reporting.AuthServer/Logs/logs.txt +host/Unity.Reporting.HttpApi.Host/Logs/logs.txt +host/Unity.Reporting.Web.Host/Logs/logs.txt +host/Unity.Reporting.Web.Unified/Logs/logs.txt +host/Unity.Reporting.Blazor.Server.Host/Logs/logs.txt + +# Use abp install-libs to restore. +**/wwwroot/libs/* diff --git a/applications/Unity.GrantManager/modules/Unity.AI/.prettierrc b/applications/Unity.GrantManager/modules/Unity.AI/.prettierrc new file mode 100644 index 0000000000..56af76bd94 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/.prettierrc @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "useTabs": false, + "tabWidth": 4 +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/NuGet.Config b/applications/Unity.GrantManager/modules/Unity.AI/NuGet.Config new file mode 100644 index 0000000000..bdc451971a --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/NuGet.Config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.AI/common.props b/applications/Unity.GrantManager/modules/Unity.AI/common.props new file mode 100644 index 0000000000..87cf88dc65 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/common.props @@ -0,0 +1,24 @@ + + + latest + 0.1.0 + $(NoWarn);CS1591 + module + + + + + + All + runtime; build; native; contentfiles; analyzers + + + + + + + $(NoWarn);0436 + + + + \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AIApplicationContractsModule.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AIApplicationContractsModule.cs new file mode 100644 index 0000000000..f9e53089f8 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AIApplicationContractsModule.cs @@ -0,0 +1,15 @@ +using Volo.Abp.Application; +using Volo.Abp.Modularity; +using Volo.Abp.Authorization; + +namespace Unity.AI; + +[DependsOn( + typeof(AIDomainSharedModule), + typeof(AbpDddApplicationContractsModule), + typeof(AbpAuthorizationModule) + )] +public class AIApplicationContractsModule : AbpModule +{ + +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AIRemoteServiceConsts.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AIRemoteServiceConsts.cs new file mode 100644 index 0000000000..118f068b39 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AIRemoteServiceConsts.cs @@ -0,0 +1,7 @@ +namespace Unity.AI; + +public static class AIRemoteServiceConsts +{ + public const string RemoteServiceName = "AI"; + public const string ModuleName = "ai"; +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/FodyWeavers.xml b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/FodyWeavers.xml new file mode 100644 index 0000000000..7e9f94ead6 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/FodyWeavers.xsd b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissionDefinitionProvider.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissionDefinitionProvider.cs new file mode 100644 index 0000000000..93f756b0af --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissionDefinitionProvider.cs @@ -0,0 +1,20 @@ +using Unity.AI.Localization; +using Volo.Abp.Authorization.Permissions; +using Volo.Abp.Localization; + +namespace Unity.AI.Permissions; + +public class AIPermissionDefinitionProvider : PermissionDefinitionProvider +{ + public override void Define(IPermissionDefinitionContext context) + { + var aiPermissionsGroup = context.AddGroup(AIPermissions.GroupName, L("Permission:AI")); + + aiPermissionsGroup.AddPermission(AIPermissions.Default.Management, L("Permission:AI.Default")); + } + + private static LocalizableString L(string name) + { + return LocalizableString.Create(name); + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissions.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissions.cs new file mode 100644 index 0000000000..8e33c15418 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissions.cs @@ -0,0 +1,11 @@ +namespace Unity.AI.Permissions; + +public static class AIPermissions +{ + public const string GroupName = "AI"; + + public static class Default + { + public const string Management = GroupName + ".Management"; + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Unity.AI.Application.Contracts.csproj b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Unity.AI.Application.Contracts.csproj new file mode 100644 index 0000000000..78bb965745 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Unity.AI.Application.Contracts.csproj @@ -0,0 +1,25 @@ + + + + + + net9.0 + enable + Unity.AI + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AIAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AIAppService.cs new file mode 100644 index 0000000000..ff3f4f8b25 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AIAppService.cs @@ -0,0 +1,11 @@ +using Volo.Abp.Application.Services; + +namespace Unity.AI; + +public abstract class AIAppService : ApplicationService +{ + protected AIAppService() + { + LocalizationResource = typeof(Localization.AIResource); + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AIApplicationAutoMapperProfile.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AIApplicationAutoMapperProfile.cs new file mode 100644 index 0000000000..874ac789db --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AIApplicationAutoMapperProfile.cs @@ -0,0 +1,11 @@ +using AutoMapper; + +namespace Unity.AI; + +public class AIApplicationAutoMapperProfile : Profile +{ + public AIApplicationAutoMapperProfile() + { + // Define AutoMapper mappings here as entities and DTOs are introduced + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AIApplicationModule.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AIApplicationModule.cs new file mode 100644 index 0000000000..60974f22b4 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AIApplicationModule.cs @@ -0,0 +1,56 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.AutoMapper; +using Volo.Abp.Modularity; +using Volo.Abp.Application; +using Volo.Abp.MultiTenancy; +using Volo.Abp.VirtualFileSystem; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.TenantManagement; + +namespace Unity.AI; + +[DependsOn( + typeof(AIApplicationContractsModule), + typeof(AbpDddApplicationModule), + typeof(AbpAutoMapperModule), + typeof(AbpTenantManagementDomainModule) + )] +public class AIApplicationModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + PreConfigure(mvcBuilder => + { + mvcBuilder.AddApplicationPartIfNotExists(typeof(AIApplicationModule).Assembly); + }); + } + + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.IsEnabled = true; + }); + + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + + context.Services.AddAutoMapperObjectMapper(); + Configure(options => + { + options.AddMaps(validate: true); + }); + + context.Services.AddHttpClientProxies( + typeof(AIApplicationContractsModule).Assembly, + AIRemoteServiceConsts.RemoteServiceName + ); + + Configure(options => + { + options.ConventionalControllers.Create(typeof(AIApplicationModule).Assembly); + }); + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Domain/AIDbProperties.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Domain/AIDbProperties.cs new file mode 100644 index 0000000000..6322b8e558 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Domain/AIDbProperties.cs @@ -0,0 +1,16 @@ +namespace Unity.AI.Domain; + +public static class AIDbProperties +{ + public static string DbTablePrefix { get; set; } = string.Empty; + + /// + /// Schema for Unity.AI tables — kept separate from other modules. + /// + public static string? DbSchema { get; set; } = "AI"; + + /// + /// Shares the Tenant connection string so no additional database infrastructure is required. + /// + public const string ConnectionStringName = "Tenant"; +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIDbContext.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIDbContext.cs new file mode 100644 index 0000000000..c12e411210 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIDbContext.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore; +using Volo.Abp.Data; +using Volo.Abp.EntityFrameworkCore; +using Unity.AI.Domain; + +namespace Unity.AI.EntityFrameworkCore; + +[ConnectionStringName(AIDbProperties.ConnectionStringName)] +public class AIDbContext(DbContextOptions options) : AbpDbContext(options), IAIDbContext +{ + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.ConfigureAI(); + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIDbContextModelCreatingExtensions.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIDbContextModelCreatingExtensions.cs new file mode 100644 index 0000000000..60b243de5d --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIDbContextModelCreatingExtensions.cs @@ -0,0 +1,20 @@ +using Microsoft.EntityFrameworkCore; +using Volo.Abp; + +namespace Unity.AI.EntityFrameworkCore; + +public static class AIDbContextModelCreatingExtensions +{ + public static void ConfigureAI(this ModelBuilder modelBuilder) + { + Check.NotNull(modelBuilder, nameof(modelBuilder)); + + // Configure AI entities here as they are introduced. + // Example: + // modelBuilder.Entity(b => + // { + // b.ToTable(AIDbProperties.DbTablePrefix + "SomeEntities", AIDbProperties.DbSchema); + // b.ConfigureByConvention(); + // }); + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIEntityFrameworkCoreModule.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIEntityFrameworkCoreModule.cs new file mode 100644 index 0000000000..9bf02d58ee --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIEntityFrameworkCoreModule.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Modularity; + +namespace Unity.AI.EntityFrameworkCore; + +public class AIEntityFrameworkCoreModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddAbpDbContext(options => + { + /* Add custom repositories here */ + }); + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/IAIDbContext.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/IAIDbContext.cs new file mode 100644 index 0000000000..2085700861 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/IAIDbContext.cs @@ -0,0 +1,11 @@ +using Volo.Abp.Data; +using Volo.Abp.EntityFrameworkCore; +using Unity.AI.Domain; + +namespace Unity.AI.EntityFrameworkCore; + +[ConnectionStringName(AIDbProperties.ConnectionStringName)] +public interface IAIDbContext : IEfCoreDbContext +{ + // Add DbSet properties here as entities are introduced +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/FodyWeavers.xml b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/FodyWeavers.xml new file mode 100644 index 0000000000..7e9f94ead6 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/FodyWeavers.xsd b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Unity.AI.Application.csproj b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Unity.AI.Application.csproj new file mode 100644 index 0000000000..e3be378d9b --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Unity.AI.Application.csproj @@ -0,0 +1,31 @@ + + + + + + net9.0 + enable + Unity.AI + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/AIErrorCodes.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/AIErrorCodes.cs new file mode 100644 index 0000000000..4dee945346 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/AIErrorCodes.cs @@ -0,0 +1,7 @@ +namespace Unity.AI; + +public static class AIErrorCodes +{ + // Define module error codes here + // Example: public const string SomeError = "AI:00001"; +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/AISharedModule.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/AISharedModule.cs new file mode 100644 index 0000000000..c33c032428 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/AISharedModule.cs @@ -0,0 +1,40 @@ +using Volo.Abp.Modularity; +using Volo.Abp.Localization; +using Unity.AI.Localization; +using Volo.Abp.Domain; +using Volo.Abp.Localization.ExceptionHandling; +using Volo.Abp.Validation; +using Volo.Abp.Validation.Localization; +using Volo.Abp.VirtualFileSystem; +using Volo.Abp.Settings; + +namespace Unity.AI; + +[DependsOn( + typeof(AbpValidationModule), + typeof(AbpDddDomainSharedModule), + typeof(AbpSettingsModule) +)] +public class AIDomainSharedModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + + Configure(options => + { + options.Resources + .Add("en") + .AddBaseTypes(typeof(AbpValidationResource)) + .AddVirtualJson("/Localization/AI"); + }); + + Configure(options => + { + options.MapCodeNamespace("AI", typeof(AIResource)); + }); + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/FodyWeavers.xml b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/FodyWeavers.xml new file mode 100644 index 0000000000..7e9f94ead6 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/FodyWeavers.xsd b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AI/en.json b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AI/en.json new file mode 100644 index 0000000000..141589bbcb --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AI/en.json @@ -0,0 +1,7 @@ +{ + "culture": "en", + "texts": { + "Permission:AI": "AI", + "Permission:AI.Default": "AI Management" + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AIResource.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AIResource.cs new file mode 100644 index 0000000000..01c5e0b812 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AIResource.cs @@ -0,0 +1,9 @@ +using Volo.Abp.Localization; + +namespace Unity.AI.Localization; + +[LocalizationResourceName("AI")] +public class AIResource +{ + +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Unity.AI.Shared.csproj b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Unity.AI.Shared.csproj new file mode 100644 index 0000000000..4cf3ed4e73 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Unity.AI.Shared.csproj @@ -0,0 +1,34 @@ + + + + + + net9.0 + enable + Unity.AI + true + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/AIWebAutoMapperProfile.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/AIWebAutoMapperProfile.cs new file mode 100644 index 0000000000..91e120f831 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/AIWebAutoMapperProfile.cs @@ -0,0 +1,11 @@ +using AutoMapper; + +namespace Unity.AI.Web; + +public class AIWebAutoMapperProfile : Profile +{ + public AIWebAutoMapperProfile() + { + // Define AutoMapper mappings for web layer here + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/AIWebModule.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/AIWebModule.cs new file mode 100644 index 0000000000..e82f53daa7 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/AIWebModule.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.DependencyInjection; +using Unity.AI.Localization; +using Volo.Abp.AspNetCore.Mvc.Localization; +using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; +using Volo.Abp.AutoMapper; +using Volo.Abp.Modularity; +using Volo.Abp.VirtualFileSystem; + +namespace Unity.AI.Web; + +[DependsOn( + typeof(AIApplicationContractsModule), + typeof(AbpAspNetCoreMvcUiThemeSharedModule), + typeof(AbpAutoMapperModule) + )] +public class AIWebModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + context.Services.PreConfigure(options => + { + options.AddAssemblyResource(typeof(AIResource), typeof(AIWebModule).Assembly); + }); + + PreConfigure(mvcBuilder => + { + mvcBuilder.AddApplicationPartIfNotExists(typeof(AIWebModule).Assembly); + }); + } + + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + + context.Services.AddAutoMapperObjectMapper(); + Configure(options => + { + options.AddMaps(validate: true); + }); + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/FodyWeavers.xml b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/FodyWeavers.xml new file mode 100644 index 0000000000..7e9f94ead6 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/FodyWeavers.xsd b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/_ViewImports.cshtml b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/_ViewImports.cshtml new file mode 100644 index 0000000000..7aa11381e3 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/_ViewImports.cshtml @@ -0,0 +1,6 @@ +@using Unity.AI.Web +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Pages.Shared.Components +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bootstrap +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Properties/launchSettings.json b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Properties/launchSettings.json new file mode 100644 index 0000000000..29ff06b0cc --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Unity.AI.Web": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:57817;http://localhost:57818" + } + } +} \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Unity.AI.Web.csproj b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Unity.AI.Web.csproj new file mode 100644 index 0000000000..b1233b386d --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Unity.AI.Web.csproj @@ -0,0 +1,42 @@ + + + + + + net9.0 + enable + true + Library + Unity.AI.Web + true + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantTenantDbContext.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantTenantDbContext.cs index ee2bdd7ed2..31f7143595 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantTenantDbContext.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantTenantDbContext.cs @@ -14,6 +14,7 @@ using Unity.Flex.EntityFrameworkCore; using Unity.Notifications.EntityFrameworkCore; using Unity.Reporting.EntityFrameworkCore; +using Unity.AI.EntityFrameworkCore; using Unity.GrantManager.GlobalTag; using Unity.GrantManager.Contacts; @@ -379,6 +380,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.ConfigureFlex(); modelBuilder.ConfigureNotifications(); modelBuilder.ConfigureReporting(); + modelBuilder.ConfigureAI(); } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260306195601_AISchema.Designer.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260306195601_AISchema.Designer.cs new file mode 100644 index 0000000000..ff79404fd3 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260306195601_AISchema.Designer.cs @@ -0,0 +1,2694 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Unity.GrantManager.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +#nullable disable + +namespace Unity.GrantManager.Migrations.HostMigrations +{ + [DbContext(typeof(GrantManagerDbContext))] + [Migration("20260306195601_AISchema")] + partial class AISchema + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.PostgreSql) + .HasAnnotation("ProductVersion", "9.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_blob_triggers", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("qrtz_calendars", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_cron_triggers", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_j_req_recovery"); + + b.ToTable("qrtz_job_details", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("qrtz_locks", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("qrtz_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("qrtz_scheduler_state", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_simple_triggers", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("smallint") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_qrtz_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_qrtz_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_qrtz_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("qrtz_triggers", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applicants.ApplicantTenantMap", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastUpdated") + .HasColumnType("timestamp without time zone"); + + b.Property("OidcSubUsername") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TenantName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OidcSubUsername"); + + b.HasIndex("OidcSubUsername", "TenantId") + .IsUnique(); + + b.ToTable("ApplicantTenantMaps", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Integrations.CasClientCode", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClientCode") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)"); + + b.Property("ClientId") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FinancialMinistry") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastUpdatedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("MinistryPrefix") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)"); + + b.HasKey("Id"); + + b.ToTable("CasClientCodes", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Integrations.DynamicUrl", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("KeyName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("DynamicUrls", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Locality.Community", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegionalDistrictCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Communities", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Locality.EconomicRegion", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("EconomicRegionCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("EconomicRegionName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.HasKey("Id"); + + b.ToTable("EconomicRegions", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Locality.ElectoralDistrict", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ElectoralDistrictCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ElectoralDistrictName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.HasKey("Id"); + + b.ToTable("ElectoralDistricts", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Locality.RegionalDistrict", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("EconomicRegionCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("RegionalDistrictCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegionalDistrictName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("RegionalDistricts", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Locality.Sector", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("SectorCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SectorName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Sectors", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Locality.SubSector", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("SectorId") + .HasColumnType("uuid"); + + b.Property("SubSectorCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SubSectorName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("SectorId"); + + b.ToTable("SubSectors", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Tokens.TenantToken", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("TenantTokens", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApplicationName") + .HasMaxLength(96) + .HasColumnType("character varying(96)") + .HasColumnName("ApplicationName"); + + b.Property("BrowserInfo") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("BrowserInfo"); + + b.Property("ClientId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("ClientId"); + + b.Property("ClientIpAddress") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("ClientIpAddress"); + + b.Property("ClientName") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("ClientName"); + + b.Property("Comments") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("Comments"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("CorrelationId"); + + b.Property("Exceptions") + .HasColumnType("text"); + + b.Property("ExecutionDuration") + .HasColumnType("integer") + .HasColumnName("ExecutionDuration"); + + b.Property("ExecutionTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("HttpMethod") + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasColumnName("HttpMethod"); + + b.Property("HttpStatusCode") + .HasColumnType("integer") + .HasColumnName("HttpStatusCode"); + + b.Property("ImpersonatorTenantId") + .HasColumnType("uuid") + .HasColumnName("ImpersonatorTenantId"); + + b.Property("ImpersonatorTenantName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("ImpersonatorTenantName"); + + b.Property("ImpersonatorUserId") + .HasColumnType("uuid") + .HasColumnName("ImpersonatorUserId"); + + b.Property("ImpersonatorUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("ImpersonatorUserName"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TenantName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("TenantName"); + + b.Property("Url") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("Url"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("UserName"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ExecutionTime"); + + b.HasIndex("TenantId", "UserId", "ExecutionTime"); + + b.ToTable("AuditLogs", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.AuditLogAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuditLogId") + .HasColumnType("uuid") + .HasColumnName("AuditLogId"); + + b.Property("ExecutionDuration") + .HasColumnType("integer") + .HasColumnName("ExecutionDuration"); + + b.Property("ExecutionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("ExecutionTime"); + + b.Property("ExtraProperties") + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("MethodName") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("MethodName"); + + b.Property("Parameters") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("Parameters"); + + b.Property("ServiceName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("ServiceName"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AuditLogId"); + + b.HasIndex("TenantId", "ServiceName", "MethodName", "ExecutionTime"); + + b.ToTable("AuditLogActions", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.EntityChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuditLogId") + .HasColumnType("uuid") + .HasColumnName("AuditLogId"); + + b.Property("ChangeTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("ChangeTime"); + + b.Property("ChangeType") + .HasColumnType("smallint") + .HasColumnName("ChangeType"); + + b.Property("EntityId") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("EntityId"); + + b.Property("EntityTenantId") + .HasColumnType("uuid"); + + b.Property("EntityTypeFullName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("EntityTypeFullName"); + + b.Property("ExtraProperties") + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AuditLogId"); + + b.HasIndex("TenantId", "EntityTypeFullName", "EntityId"); + + b.ToTable("EntityChanges", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.EntityPropertyChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EntityChangeId") + .HasColumnType("uuid"); + + b.Property("NewValue") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("NewValue"); + + b.Property("OriginalValue") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("OriginalValue"); + + b.Property("PropertyName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("PropertyName"); + + b.Property("PropertyTypeFullName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("PropertyTypeFullName"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("EntityChangeId"); + + b.ToTable("EntityPropertyChanges", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.BackgroundJobs.BackgroundJobRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsAbandoned") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JobArgs") + .IsRequired() + .HasMaxLength(1048576) + .HasColumnType("character varying(1048576)"); + + b.Property("JobName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("LastTryTime") + .HasColumnType("timestamp without time zone"); + + b.Property("NextTryTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Priority") + .ValueGeneratedOnAdd() + .HasColumnType("smallint") + .HasDefaultValue((byte)15); + + b.Property("TryCount") + .ValueGeneratedOnAdd() + .HasColumnType("smallint") + .HasDefaultValue((short)0); + + b.HasKey("Id"); + + b.HasIndex("IsAbandoned", "NextTryTime"); + + b.ToTable("BackgroundJobs", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.FeatureManagement.FeatureDefinitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AllowedProviders") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DefaultValue") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExtraProperties") + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("IsAvailableToHost") + .HasColumnType("boolean"); + + b.Property("IsVisibleToClients") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ParentName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ValueType") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.HasKey("Id"); + + b.HasIndex("GroupName"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Features", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.FeatureManagement.FeatureGroupDefinitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExtraProperties") + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("FeatureGroups", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.FeatureManagement.FeatureValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ProviderKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ProviderName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.HasKey("Id"); + + b.HasIndex("Name", "ProviderName", "ProviderKey") + .IsUnique(); + + b.ToTable("FeatureValues", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityClaimType", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsStatic") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Regex") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("RegexDescription") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Required") + .HasColumnType("boolean"); + + b.Property("ValueType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ClaimTypes", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityLinkUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("SourceTenantId") + .HasColumnType("uuid"); + + b.Property("SourceUserId") + .HasColumnType("uuid"); + + b.Property("TargetTenantId") + .HasColumnType("uuid"); + + b.Property("TargetUserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("SourceUserId", "SourceTenantId", "TargetUserId", "TargetTenantId") + .IsUnique(); + + b.ToTable("LinkUsers", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("EntityVersion") + .HasColumnType("integer"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDefault") + .HasColumnType("boolean") + .HasColumnName("IsDefault"); + + b.Property("IsPublic") + .HasColumnType("boolean") + .HasColumnName("IsPublic"); + + b.Property("IsStatic") + .HasColumnType("boolean") + .HasColumnName("IsStatic"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName"); + + b.ToTable("Roles", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClaimType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ClaimValue") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("RoleClaims", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentitySecurityLog", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Action") + .HasMaxLength(96) + .HasColumnType("character varying(96)"); + + b.Property("ApplicationName") + .HasMaxLength(96) + .HasColumnType("character varying(96)"); + + b.Property("BrowserInfo") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("ClientId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ClientIpAddress") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("Identity") + .HasMaxLength(96) + .HasColumnType("character varying(96)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TenantName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Action"); + + b.HasIndex("TenantId", "ApplicationName"); + + b.HasIndex("TenantId", "Identity"); + + b.HasIndex("TenantId", "UserId"); + + b.ToTable("SecurityLogs", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentitySession", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClientId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Device") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("DeviceInfo") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ExtraProperties") + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IpAddresses") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("LastAccessed") + .HasColumnType("timestamp without time zone"); + + b.Property("SessionId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("SignedIn") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Device"); + + b.HasIndex("SessionId"); + + b.HasIndex("TenantId", "UserId"); + + b.ToTable("Sessions", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessFailedCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("Email"); + + b.Property("EmailConfirmed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("EmailConfirmed"); + + b.Property("EntityVersion") + .HasColumnType("integer"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("IsActive"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsExternal") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsExternal"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastPasswordChangeTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LockoutEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("LockoutEnabled"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("Name"); + + b.Property("NormalizedEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("NormalizedEmail"); + + b.Property("NormalizedUserName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("NormalizedUserName"); + + b.Property("OidcSub") + .HasColumnType("text"); + + b.Property("PasswordHash") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("PasswordHash"); + + b.Property("PhoneNumber") + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasColumnName("PhoneNumber"); + + b.Property("PhoneNumberConfirmed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("PhoneNumberConfirmed"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("SecurityStamp"); + + b.Property("ShouldChangePasswordOnNextLogin") + .HasColumnType("boolean"); + + b.Property("Surname") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("Surname"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TwoFactorEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("TwoFactorEnabled"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("UserName"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("NormalizedEmail"); + + b.HasIndex("NormalizedUserName"); + + b.HasIndex("UserName"); + + b.ToTable("Users", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClaimType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ClaimValue") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserDelegation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("EndTime") + .HasColumnType("timestamp without time zone"); + + b.Property("SourceUserId") + .HasColumnType("uuid"); + + b.Property("StartTime") + .HasColumnType("timestamp without time zone"); + + b.Property("TargetUserId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("UserDelegations", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserLogin", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("LoginProvider") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ProviderDisplayName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ProviderKey") + .IsRequired() + .HasMaxLength(196) + .HasColumnType("character varying(196)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("UserId", "LoginProvider"); + + b.HasIndex("LoginProvider", "ProviderKey"); + + b.ToTable("UserLogins", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserOrganizationUnit", b => + { + b.Property("OrganizationUnitId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("OrganizationUnitId", "UserId"); + + b.HasIndex("UserId", "OrganizationUnitId"); + + b.ToTable("UserOrganizationUnits", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId", "UserId"); + + b.ToTable("UserRoles", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("LoginProvider") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Name") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("UserTokens", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnit", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(95) + .HasColumnType("character varying(95)") + .HasColumnName("Code"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("DisplayName"); + + b.Property("EntityVersion") + .HasColumnType("integer"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ParentId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("Code"); + + b.HasIndex("ParentId"); + + b.ToTable("OrganizationUnits", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnitRole", b => + { + b.Property("OrganizationUnitId") + .HasColumnType("uuid"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("OrganizationUnitId", "RoleId"); + + b.HasIndex("RoleId", "OrganizationUnitId"); + + b.ToTable("OrganizationUnitRoles", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.PermissionManagement.PermissionDefinitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExtraProperties") + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("MultiTenancySide") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ParentName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Providers") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("StateCheckers") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("GroupName"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Permissions", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.PermissionManagement.PermissionGrant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ProviderKey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ProviderName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Name", "ProviderName", "ProviderKey") + .IsUnique(); + + b.ToTable("PermissionGrants", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.PermissionManagement.PermissionGroupDefinitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExtraProperties") + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("PermissionGroups", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.SettingManagement.Setting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ProviderKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ProviderName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.HasKey("Id"); + + b.HasIndex("Name", "ProviderName", "ProviderKey") + .IsUnique(); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.SettingManagement.SettingDefinitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DefaultValue") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExtraProperties") + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsEncrypted") + .HasColumnType("boolean"); + + b.Property("IsInherited") + .HasColumnType("boolean"); + + b.Property("IsVisibleToClients") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Providers") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("SettingDefinitions", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.TenantManagement.Tenant", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EntityVersion") + .HasColumnType("integer"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.HasIndex("NormalizedName"); + + b.ToTable("Tenants", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.TenantManagement.TenantConnectionString", b => + { + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Name") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("TenantId", "Name"); + + b.ToTable("TenantConnectionStrings", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("Unity.GrantManager.Locality.SubSector", b => + { + b.HasOne("Unity.GrantManager.Locality.Sector", "Sector") + .WithMany("SubSectors") + .HasForeignKey("SectorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Sector"); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.AuditLogAction", b => + { + b.HasOne("Volo.Abp.AuditLogging.AuditLog", null) + .WithMany("Actions") + .HasForeignKey("AuditLogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.EntityChange", b => + { + b.HasOne("Volo.Abp.AuditLogging.AuditLog", null) + .WithMany("EntityChanges") + .HasForeignKey("AuditLogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.EntityPropertyChange", b => + { + b.HasOne("Volo.Abp.AuditLogging.EntityChange", null) + .WithMany("PropertyChanges") + .HasForeignKey("EntityChangeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityRoleClaim", b => + { + b.HasOne("Volo.Abp.Identity.IdentityRole", null) + .WithMany("Claims") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserClaim", b => + { + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("Claims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserLogin", b => + { + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("Logins") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserOrganizationUnit", b => + { + b.HasOne("Volo.Abp.Identity.OrganizationUnit", null) + .WithMany() + .HasForeignKey("OrganizationUnitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("OrganizationUnits") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserRole", b => + { + b.HasOne("Volo.Abp.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("Roles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserToken", b => + { + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("Tokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnit", b => + { + b.HasOne("Volo.Abp.Identity.OrganizationUnit", null) + .WithMany() + .HasForeignKey("ParentId"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnitRole", b => + { + b.HasOne("Volo.Abp.Identity.OrganizationUnit", null) + .WithMany("Roles") + .HasForeignKey("OrganizationUnitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Volo.Abp.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.TenantManagement.TenantConnectionString", b => + { + b.HasOne("Volo.Abp.TenantManagement.Tenant", null) + .WithMany("ConnectionStrings") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); + + modelBuilder.Entity("Unity.GrantManager.Locality.Sector", b => + { + b.Navigation("SubSectors"); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.AuditLog", b => + { + b.Navigation("Actions"); + + b.Navigation("EntityChanges"); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.EntityChange", b => + { + b.Navigation("PropertyChanges"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityRole", b => + { + b.Navigation("Claims"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUser", b => + { + b.Navigation("Claims"); + + b.Navigation("Logins"); + + b.Navigation("OrganizationUnits"); + + b.Navigation("Roles"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnit", b => + { + b.Navigation("Roles"); + }); + + modelBuilder.Entity("Volo.Abp.TenantManagement.Tenant", b => + { + b.Navigation("ConnectionStrings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260306195601_AISchema.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260306195601_AISchema.cs new file mode 100644 index 0000000000..1c85f5cb8a --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260306195601_AISchema.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Unity.GrantManager.Migrations.HostMigrations +{ + /// + public partial class AISchema : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema(name: "AI"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/GrantManagerDbContextModelSnapshot.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/GrantManagerDbContextModelSnapshot.cs index f4cf613605..d034c14e8e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/GrantManagerDbContextModelSnapshot.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/GrantManagerDbContextModelSnapshot.cs @@ -472,16 +472,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Id") .HasColumnType("uuid"); - b.Property("ClientCode") - .IsRequired() - .HasColumnType("text"); - - b.Property("ClientId") - .HasColumnType("text"); - b.Property("ConcurrencyStamp") + .IsConcurrencyToken() .IsRequired() - .HasColumnType("text"); + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); b.Property("CreationTime") .HasColumnType("timestamp without time zone") @@ -491,15 +487,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid") .HasColumnName("CreatorId"); - b.Property("Description") + b.Property("ExtraProperties") .IsRequired() - .HasColumnType("text"); - - b.Property("FinancialMinistry") - .HasColumnType("text"); - - b.Property("IsActive") - .HasColumnType("boolean"); + .HasColumnType("text") + .HasColumnName("ExtraProperties"); b.Property("LastUpdated") .HasColumnType("timestamp without time zone"); @@ -525,6 +516,84 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("ApplicantTenantMaps", (string)null); }); + modelBuilder.Entity("Unity.GrantManager.Integrations.CasClientCode", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClientCode") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)"); + + b.Property("ClientId") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FinancialMinistry") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastUpdatedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("MinistryPrefix") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)"); + + b.HasKey("Id"); + + b.ToTable("CasClientCodes", (string)null); + }); + modelBuilder.Entity("Unity.GrantManager.Integrations.DynamicUrl", b => { b.Property("Id") diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Unity.GrantManager.EntityFrameworkCore.csproj b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Unity.GrantManager.EntityFrameworkCore.csproj index 6d06183c85..7a957daef7 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Unity.GrantManager.EntityFrameworkCore.csproj +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Unity.GrantManager.EntityFrameworkCore.csproj @@ -43,6 +43,7 @@ + diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Unity.GrantManager.Web.csproj b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Unity.GrantManager.Web.csproj index 7849bfb978..f01ec57688 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Unity.GrantManager.Web.csproj +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Unity.GrantManager.Web.csproj @@ -25,6 +25,7 @@ + From fc2b30e5679fe71398962d3e159f214c3b7235d1 Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Fri, 6 Mar 2026 12:50:53 -0800 Subject: [PATCH 02/58] feature/AB#32216-BuildAIBase-RemoveDBContext-ReviewFixes --- .../Permissions/AIPermissions.cs | 7 +++++++ .../Domain/AIDbProperties.cs | 5 ----- .../EntityFrameworkCore/AIDbContext.cs | 17 ----------------- .../AIEntityFrameworkCoreModule.cs | 15 --------------- .../EntityFrameworkCore/IAIDbContext.cs | 11 ----------- .../GrantManagerWebModule.cs | 4 +++- 6 files changed, 10 insertions(+), 49 deletions(-) delete mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIDbContext.cs delete mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIEntityFrameworkCoreModule.cs delete mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/IAIDbContext.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissions.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissions.cs index 8e33c15418..78b064262e 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissions.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissions.cs @@ -1,3 +1,5 @@ +using Volo.Abp.Reflection; + namespace Unity.AI.Permissions; public static class AIPermissions @@ -8,4 +10,9 @@ public static class Default { public const string Management = GroupName + ".Management"; } + + public static string[] GetAll() + { + return ReflectionHelper.GetPublicConstantsRecursively(typeof(AIPermissions)); + } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Domain/AIDbProperties.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Domain/AIDbProperties.cs index 6322b8e558..d786709167 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Domain/AIDbProperties.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Domain/AIDbProperties.cs @@ -8,9 +8,4 @@ public static class AIDbProperties /// Schema for Unity.AI tables — kept separate from other modules. /// public static string? DbSchema { get; set; } = "AI"; - - /// - /// Shares the Tenant connection string so no additional database infrastructure is required. - /// - public const string ConnectionStringName = "Tenant"; } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIDbContext.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIDbContext.cs deleted file mode 100644 index c12e411210..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIDbContext.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Volo.Abp.Data; -using Volo.Abp.EntityFrameworkCore; -using Unity.AI.Domain; - -namespace Unity.AI.EntityFrameworkCore; - -[ConnectionStringName(AIDbProperties.ConnectionStringName)] -public class AIDbContext(DbContextOptions options) : AbpDbContext(options), IAIDbContext -{ - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.ConfigureAI(); - } -} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIEntityFrameworkCoreModule.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIEntityFrameworkCoreModule.cs deleted file mode 100644 index 9bf02d58ee..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIEntityFrameworkCoreModule.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.Modularity; - -namespace Unity.AI.EntityFrameworkCore; - -public class AIEntityFrameworkCoreModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - context.Services.AddAbpDbContext(options => - { - /* Add custom repositories here */ - }); - } -} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/IAIDbContext.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/IAIDbContext.cs deleted file mode 100644 index 2085700861..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/IAIDbContext.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Volo.Abp.Data; -using Volo.Abp.EntityFrameworkCore; -using Unity.AI.Domain; - -namespace Unity.AI.EntityFrameworkCore; - -[ConnectionStringName(AIDbProperties.ConnectionStringName)] -public interface IAIDbContext : IEfCoreDbContext -{ - // Add DbSet properties here as entities are introduced -} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebModule.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebModule.cs index caf91fc168..826590b751 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebModule.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebModule.cs @@ -75,6 +75,7 @@ using Unity.Modules.Shared.Utils; using Unity.Notifications.Web.Bundling; using Unity.Reporting.Web; +using Unity.AI.Web; using Unity.GrantManager.Web.Views.Settings; namespace Unity.GrantManager.Web; @@ -99,7 +100,8 @@ namespace Unity.GrantManager.Web; typeof(AbpBlobStoringModule), typeof(NotificationsWebModule), typeof(FlexWebModule), - typeof(ReportingWebModule) + typeof(ReportingWebModule), + typeof(AIWebModule) )] public class GrantManagerWebModule : AbpModule From 8c00f95775d9efe60302defc9bf0de47ac1fdd82 Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Fri, 6 Mar 2026 13:38:04 -0800 Subject: [PATCH 03/58] feature/AB#32216-BuildAIBase-Permission:AI Already existed as a group --- .../AIPermissionDefinitionProvider.cs | 28 +++++++++++++++++-- .../Permissions/AIPermissions.cs | 22 +++++++++++++++ .../Unity.AI.Application.Contracts.csproj | 1 + .../Localization/AI/en.json | 6 +++- ...ApplicationPermissionDefinitionProvider.cs | 27 +----------------- .../Assessments/AssessmentAppService.cs | 6 ++-- .../Unity.GrantManager.Application.csproj | 1 + .../Localization/GrantManager/en.json | 5 ---- .../GrantApplicationPermissions.cs | 26 +---------------- .../Menus/GrantManagerMenuContributor.cs | 9 +++--- .../Pages/GrantApplications/Details.cshtml | 3 +- .../ChefsAttachments/ChefsAttachments.cs | 3 +- 12 files changed, 70 insertions(+), 67 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissionDefinitionProvider.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissionDefinitionProvider.cs index 93f756b0af..8c56c25ffc 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissionDefinitionProvider.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissionDefinitionProvider.cs @@ -1,6 +1,7 @@ using Unity.AI.Localization; using Volo.Abp.Authorization.Permissions; using Volo.Abp.Localization; +using Volo.Abp.Features; namespace Unity.AI.Permissions; @@ -8,9 +9,32 @@ public class AIPermissionDefinitionProvider : PermissionDefinitionProvider { public override void Define(IPermissionDefinitionContext context) { - var aiPermissionsGroup = context.AddGroup(AIPermissions.GroupName, L("Permission:AI")); + // AI Permission Group + var aiPermissionsGroup = context.AddGroup( + AIPermissions.GroupName, + L("Permission:AI")); + + + aiPermissionsGroup.AddPermission( + AIPermissions.Default.Reporting.Default, + L("Permission:AI.Reporting")) + .RequireFeatures("Unity.AIReporting"); + + aiPermissionsGroup.AddPermission( + AIPermissions.Default.ApplicationAnalysis.Default, + L("Permission:AI.ApplicationAnalysis")) + .RequireFeatures("Unity.AI.ApplicationAnalysis"); + + aiPermissionsGroup.AddPermission( + AIPermissions.Default.AttachmentSummary.Default , + L("Permission:AI.AttachmentSummary")) + .RequireFeatures("Unity.AI.AttachmentSummaries"); + + aiPermissionsGroup.AddPermission( + AIPermissions.Default.ScoringAssistant.Default, + L("Permission:AI.ScoringAssistant")) + .RequireFeatures("Unity.AI.Scoring"); - aiPermissionsGroup.AddPermission(AIPermissions.Default.Management, L("Permission:AI.Default")); } private static LocalizableString L(string name) diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissions.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissions.cs index 78b064262e..8e94d7fe77 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissions.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissions.cs @@ -1,4 +1,5 @@ using Volo.Abp.Reflection; +using Volo.Abp.Features; namespace Unity.AI.Permissions; @@ -9,6 +10,27 @@ public static class AIPermissions public static class Default { public const string Management = GroupName + ".Management"; + public const string GroupName = "AI"; + + public static class Reporting + { + public const string Default = GroupName + ".Reporting"; + } + + public static class ApplicationAnalysis + { + public const string Default = GroupName + ".ApplicationAnalysis"; + } + + public static class AttachmentSummary + { + public const string Default = GroupName + ".AttachmentSummary"; + } + + public static class ScoringAssistant + { + public const string Default = GroupName + ".ScoringAssistant"; + } } public static string[] GetAll() diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Unity.AI.Application.Contracts.csproj b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Unity.AI.Application.Contracts.csproj index 78bb965745..cd71888469 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Unity.AI.Application.Contracts.csproj +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Unity.AI.Application.Contracts.csproj @@ -11,6 +11,7 @@ + diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AI/en.json b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AI/en.json index 141589bbcb..775b9a4d87 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AI/en.json +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AI/en.json @@ -2,6 +2,10 @@ "culture": "en", "texts": { "Permission:AI": "AI", - "Permission:AI.Default": "AI Management" + "Permission:AI.Default": "AI Management", + "Permission:AI.Reporting": "AI Reporting", + "Permission:AI.ApplicationAnalysis": "AI Application Analysis", + "Permission:AI.AttachmentSummary": "AI Attachment Summary", + "Permission:AI.ScoringAssistant": "AI Scoring Assistant" } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantApplications/GrantApplicationPermissionDefinitionProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantApplications/GrantApplicationPermissionDefinitionProvider.cs index 710a9bfca0..d21954da46 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantApplications/GrantApplicationPermissionDefinitionProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantApplications/GrantApplicationPermissionDefinitionProvider.cs @@ -115,32 +115,7 @@ public override void Define(IPermissionDefinitionContext context) //-- TAG ASSIGNMENT var tagsPermissionsGroup = context.AddGroup("Tags", L("Permission:Tags")); tagsPermissionsGroup.AddPermission(UnitySelector.Application.Tags.Create, L(UnitySelector.Application.Tags.Create)); - tagsPermissionsGroup.AddPermission(UnitySelector.Application.Tags.Delete, L(UnitySelector.Application.Tags.Delete)); - - // AI Permission Group - var aiPermissionsGroup = context.AddGroup( - GrantApplicationPermissions.AI.GroupName, - L("Permission:AI")); - - aiPermissionsGroup.AddPermission( - GrantApplicationPermissions.AI.Reporting.Default, - L("Permission:AI.Reporting")) - .RequireFeatures("Unity.AIReporting"); - - aiPermissionsGroup.AddPermission( - GrantApplicationPermissions.AI.ApplicationAnalysis.Default, - L("Permission:AI.ApplicationAnalysis")) - .RequireFeatures("Unity.AI.ApplicationAnalysis"); - - aiPermissionsGroup.AddPermission( - GrantApplicationPermissions.AI.AttachmentSummary.Default, - L("Permission:AI.AttachmentSummary")) - .RequireFeatures("Unity.AI.AttachmentSummaries"); - - aiPermissionsGroup.AddPermission( - GrantApplicationPermissions.AI.ScoringAssistant.Default, - L("Permission:AI.ScoringAssistant")) - .RequireFeatures("Unity.AI.Scoring"); + tagsPermissionsGroup.AddPermission(UnitySelector.Application.Tags.Delete, L(UnitySelector.Application.Tags.Delete)); } private static LocalizableString L(string name) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Assessments/AssessmentAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Assessments/AssessmentAppService.cs index c5a2c26f3b..95b07c4845 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Assessments/AssessmentAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Assessments/AssessmentAppService.cs @@ -1,11 +1,11 @@ using Microsoft.AspNetCore.Authorization; -using Volo.Abp; using Microsoft.AspNetCore.Authorization.Infrastructure; using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Threading.Tasks; +using Unity.AI.Permissions; using Unity.Flex; using Unity.Flex.Scoresheets; using Unity.Flex.Scoresheets.Enums; @@ -17,7 +17,9 @@ using Unity.GrantManager.Permissions; using Unity.GrantManager.Workflow; using Unity.Modules.Shared; +using Volo.Abp; using Volo.Abp.Application.Services; +using Volo.Abp.Authorization.Permissions; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Repositories; using Volo.Abp.EventBus.Local; @@ -94,7 +96,7 @@ public async Task GetDisplayList(Guid applicationId) // If AI Scoring feature is disabled, or user doesn't have permissions to view AI assessments, filter out AI assessments from the list var aiScoringEnabled = await _featureChecker.IsEnabledAsync("Unity.AI.Scoring"); - var canViewAI = await AuthorizationService.IsGrantedAsync(GrantApplicationPermissions.AI.ScoringAssistant.Default); + var canViewAI = await AuthorizationService.IsGrantedAsync(AIPermissions.Default.ScoringAssistant.Default); assessmentList = assessmentList .Where(a => !a.IsAiAssessment || (aiScoringEnabled && canViewAI)) .OrderByDescending(a => a.IsAiAssessment) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Unity.GrantManager.Application.csproj b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Unity.GrantManager.Application.csproj index ff57bfd948..081068b468 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Unity.GrantManager.Application.csproj +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Unity.GrantManager.Application.csproj @@ -12,6 +12,7 @@ + diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json index 85143a12dd..00e7708474 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json @@ -153,11 +153,6 @@ "Permission:GrantManagerManagement.ApplicationForms.Default": "Manage Forms", "Permission:GrantApplicationManagement.Approvals.BulkApplicationApproval": "Bulk Application Approval", "Permission:GrantApplicationManagement.AIReporting.Default": "AI Reporting", - "Permission:AI": "AI", - "Permission:AI.Reporting": "AI Reporting", - "Permission:AI.ApplicationAnalysis": "AI Application Analysis", - "Permission:AI.AttachmentSummary": "AI Attachment Summary", - "Permission:AI.ScoringAssistant": "AI Scoring Assistant", "ApplicationForms": "Forms", "ApplicationForms:Description": "Description", diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/GrantApplicationPermissions.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/GrantApplicationPermissions.cs index 171c99656a..73afc90266 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/GrantApplicationPermissions.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/GrantApplicationPermissions.cs @@ -41,31 +41,7 @@ public static class Applicants public const string ApplicantInfoDefault = Default + ".ApplicantInfo"; public const string EditRedStop = ApplicantInfoDefault + ".EditRedStop"; } - - public static class AI - { - public const string GroupName = "AI"; - - public static class Reporting - { - public const string Default = GroupName + ".Reporting"; - } - - public static class ApplicationAnalysis - { - public const string Default = GroupName + ".ApplicationAnalysis"; - } - - public static class AttachmentSummary - { - public const string Default = GroupName + ".AttachmentSummary"; - } - - public static class ScoringAssistant - { - public const string Default = GroupName + ".ScoringAssistant"; - } - } + public static class Assignments { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenuContributor.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenuContributor.cs index f881f05e38..6233d6ca6e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenuContributor.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenuContributor.cs @@ -1,14 +1,15 @@ -using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using System.Threading.Tasks; +using Unity.AI.Permissions; using Unity.GrantManager.Localization; using Unity.GrantManager.Permissions; using Unity.Identity.Web.Navigation; using Unity.Modules.Shared.Permissions; using Unity.TenantManagement; using Unity.TenantManagement.Web.Navigation; +using Volo.Abp.Features; using Volo.Abp.Identity; using Volo.Abp.UI.Navigation; -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.Features; namespace Unity.GrantManager.Web.Menus; @@ -124,7 +125,7 @@ private async static Task ConfigureMainMenuAsync(MenuConfigurationContext contex l["Menu:AIReporting"], "~/AIReporting", icon: "fl fl-view-dashboard", - requiredPermissionName: GrantApplicationPermissions.AI.Reporting.Default, + requiredPermissionName: AIPermissions.Default.Reporting.Default, order: 9 ) ); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml index f4894efe2d..d3a22c44b2 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml @@ -1,6 +1,7 @@ @page "{@Model.ApplicationFormSubmissionId?}" @* #pragma warning disable S1128 *@ @using Microsoft.Extensions.Localization +@using Unity.AI.Permissions @using Unity.Flex.Web.Views.Shared.Components.WorksheetInstanceWidget @using Unity.GrantManager.Flex @using Unity.GrantManager.Localization @@ -31,7 +32,7 @@ var notificationsFeatureEnabled = await FeatureChecker.IsEnabledAsync("Unity.Notifications"); var readEmailGranted = await PermissionChecker.IsGrantedAsync("Notifications.Email"); var aiApplicationAnalysisEnabled = await FeatureChecker.IsEnabledAsync("Unity.AI.ApplicationAnalysis") - && await PermissionChecker.IsGrantedAsync(GrantApplicationPermissions.AI.ApplicationAnalysis.Default); + && await PermissionChecker.IsGrantedAsync(AIPermissions.Default.ApplicationAnalysis.Default); var flexFeatureEnabled = await FeatureChecker.IsEnabledAsync("Unity.Flex"); } @section styles diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.cs index 57d32cff70..001cf10a62 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.cs @@ -7,6 +7,7 @@ using Volo.Abp.Features; using Volo.Abp.Authorization.Permissions; using Unity.GrantManager.Permissions; +using Unity.AI.Permissions; namespace Unity.GrantManager.Web.Views.Shared.Components.ChefsAttachments { @@ -29,7 +30,7 @@ public async Task InvokeAsync() { var isAIAttachmentSummariesEnabled = await _featureChecker.IsEnabledAsync("Unity.AI.AttachmentSummaries") && - await _permissionChecker.IsGrantedAsync(GrantApplicationPermissions.AI.AttachmentSummary.Default); + await _permissionChecker.IsGrantedAsync(AIPermissions.Default.AttachmentSummary.Default); ViewBag.IsAIAttachmentSummariesEnabled = isAIAttachmentSummariesEnabled; return View(); } From 3103c87f8fdbf291200e95ae38c7be88b2eff3ac Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Fri, 6 Mar 2026 14:05:31 -0800 Subject: [PATCH 04/58] feature/AB#32216-BuildAIBase-Permission:SonarFixes --- .../Unity.GrantManager/Unity.GrantManager.sln | 4 +- .../AIPermissionDefinitionProvider.cs | 8 ++-- .../Permissions/AIPermissions.cs | 44 +++++++++---------- .../AIDbContextModelCreatingExtensions.cs | 8 +--- .../Unity.AI.Domain.Shared/AIErrorCodes.cs | 7 --- .../modules/Unity.Flex/Unity.Flex.sln | 2 +- .../Unity.Notifications.sln | 2 +- .../modules/Unity.Payments/Unity.Payments.sln | 2 +- .../Unity.Reporting/Unity.Reporting.sln | 2 +- .../Assessments/AssessmentAppService.cs | 4 +- .../Menus/GrantManagerMenuContributor.cs | 2 +- .../Pages/GrantApplications/Details.cshtml | 2 +- .../ChefsAttachments/ChefsAttachments.cs | 2 +- 13 files changed, 36 insertions(+), 53 deletions(-) delete mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/AIErrorCodes.cs diff --git a/applications/Unity.GrantManager/Unity.GrantManager.sln b/applications/Unity.GrantManager/Unity.GrantManager.sln index 72ef37a99f..9fa0d5294b 100644 --- a/applications/Unity.GrantManager/Unity.GrantManager.sln +++ b/applications/Unity.GrantManager/Unity.GrantManager.sln @@ -153,7 +153,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unity.Payments.Shared", "mo EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unity.Flex.Web.Tests", "modules\Unity.Flex\test\Unity.Flex.Web.Tests\Unity.Flex.Web.Tests.csproj", "{5F4CFB7E-A14A-40A1-8833-A55CB296D31B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.Reporting.Web", "modules\Unity.Reporting\src\Unity.Reporting.Web\Unity.Reporting.Web.csproj", "{3E4E5506-9820-4650-8062-4A07FB2C851A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unity.Reporting.Web", "modules\Unity.Reporting\src\Unity.Reporting.Web\Unity.Reporting.Web.csproj", "{3E4E5506-9820-4650-8062-4A07FB2C851A}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Unity.AI", "Unity.AI", "{BA2040C4-DC9D-44D2-B8A8-5A18D3D649AB}" EndProject @@ -163,7 +163,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unity.AI.Application.Contra EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unity.AI.Application", "modules\Unity.AI\src\Unity.AI.Application\Unity.AI.Application.csproj", "{7CF9D364-2018-4199-879B-371F6E1AC58B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.AI.Web", "modules\Unity.AI\src\Unity.AI.Web\Unity.AI.Web.csproj", "{378A4EB8-3DC1-420E-98B5-798DE71BEF0D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unity.AI.Web", "modules\Unity.AI\src\Unity.AI.Web\Unity.AI.Web.csproj", "{378A4EB8-3DC1-420E-98B5-798DE71BEF0D}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissionDefinitionProvider.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissionDefinitionProvider.cs index 8c56c25ffc..05a8f98c81 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissionDefinitionProvider.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissionDefinitionProvider.cs @@ -16,22 +16,22 @@ public override void Define(IPermissionDefinitionContext context) aiPermissionsGroup.AddPermission( - AIPermissions.Default.Reporting.Default, + AIPermissions.Reporting.ReportingDefault, L("Permission:AI.Reporting")) .RequireFeatures("Unity.AIReporting"); aiPermissionsGroup.AddPermission( - AIPermissions.Default.ApplicationAnalysis.Default, + AIPermissions.ApplicationAnalysis.ApplicationAnalysisDefault, L("Permission:AI.ApplicationAnalysis")) .RequireFeatures("Unity.AI.ApplicationAnalysis"); aiPermissionsGroup.AddPermission( - AIPermissions.Default.AttachmentSummary.Default , + AIPermissions.AttachmentSummary.AttachmentSummaryDefault , L("Permission:AI.AttachmentSummary")) .RequireFeatures("Unity.AI.AttachmentSummaries"); aiPermissionsGroup.AddPermission( - AIPermissions.Default.ScoringAssistant.Default, + AIPermissions.ScoringAssistant.ScoringAssistantDefault, L("Permission:AI.ScoringAssistant")) .RequireFeatures("Unity.AI.Scoring"); diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissions.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissions.cs index 8e94d7fe77..844a8d8e1f 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissions.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissions.cs @@ -1,5 +1,4 @@ using Volo.Abp.Reflection; -using Volo.Abp.Features; namespace Unity.AI.Permissions; @@ -7,32 +6,29 @@ public static class AIPermissions { public const string GroupName = "AI"; - public static class Default + public const string Management = GroupName + ".Management"; + + public static class Reporting + { + public const string ReportingDefault = GroupName + ".Reporting"; + } + + public static class ApplicationAnalysis + { + public const string ApplicationAnalysisDefault = GroupName + ".ApplicationAnalysis"; + } + + public static class AttachmentSummary { - public const string Management = GroupName + ".Management"; - public const string GroupName = "AI"; - - public static class Reporting - { - public const string Default = GroupName + ".Reporting"; - } - - public static class ApplicationAnalysis - { - public const string Default = GroupName + ".ApplicationAnalysis"; - } - - public static class AttachmentSummary - { - public const string Default = GroupName + ".AttachmentSummary"; - } - - public static class ScoringAssistant - { - public const string Default = GroupName + ".ScoringAssistant"; - } + public const string AttachmentSummaryDefault = GroupName + ".AttachmentSummary"; } + public static class ScoringAssistant + { + public const string ScoringAssistantDefault = GroupName + ".ScoringAssistant"; + } + + public static string[] GetAll() { return ReflectionHelper.GetPublicConstantsRecursively(typeof(AIPermissions)); diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIDbContextModelCreatingExtensions.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIDbContextModelCreatingExtensions.cs index 60b243de5d..bfdb1bb031 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIDbContextModelCreatingExtensions.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIDbContextModelCreatingExtensions.cs @@ -10,11 +10,7 @@ public static void ConfigureAI(this ModelBuilder modelBuilder) Check.NotNull(modelBuilder, nameof(modelBuilder)); // Configure AI entities here as they are introduced. - // Example: - // modelBuilder.Entity(b => - // { - // b.ToTable(AIDbProperties.DbTablePrefix + "SomeEntities", AIDbProperties.DbSchema); - // b.ConfigureByConvention(); - // }); + // Example: modelBuilder add Entity To table and configurations + } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/AIErrorCodes.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/AIErrorCodes.cs deleted file mode 100644 index 4dee945346..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/AIErrorCodes.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Unity.AI; - -public static class AIErrorCodes -{ - // Define module error codes here - // Example: public const string SomeError = "AI:00001"; -} diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/Unity.Flex.sln b/applications/Unity.GrantManager/modules/Unity.Flex/Unity.Flex.sln index 1b5a13db2a..201f4dfca4 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/Unity.Flex.sln +++ b/applications/Unity.GrantManager/modules/Unity.Flex/Unity.Flex.sln @@ -35,7 +35,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unity.Flex.Web", "src\Unity EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unity.Flex.HttpApi.Client.ConsoleTestApp", "test\Unity.Flex.HttpApi.Client.ConsoleTestApp\Unity.Flex.HttpApi.Client.ConsoleTestApp.csproj", "{1EDCD6D4-DF3A-4E3B-ABB6-C0D0B373EAB8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.Flex.Installer", "src\Unity.Flex.Installer\Unity.Flex.Installer.csproj", "{BE39FD00-745B-4049-8161-FC129817CBE4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unity.Flex.Installer", "src\Unity.Flex.Installer\Unity.Flex.Installer.csproj", "{BE39FD00-745B-4049-8161-FC129817CBE4}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/Unity.Notifications.sln b/applications/Unity.GrantManager/modules/Unity.Notifications/Unity.Notifications.sln index 9e3121eb10..b3370928f9 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/Unity.Notifications.sln +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/Unity.Notifications.sln @@ -35,7 +35,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unity.Notifications.Web", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unity.Notifications.HttpApi.Client.ConsoleTestApp", "test\Unity.Notifications.HttpApi.Client.ConsoleTestApp\Unity.Notifications.HttpApi.Client.ConsoleTestApp.csproj", "{1EDCD6D4-DF3A-4E3B-ABB6-C0D0B373EAB8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.Notifications.Installer", "src\Unity.Notifications.Installer\Unity.Notifications.Installer.csproj", "{BE39FD00-745B-4049-8161-FC129817CBE4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unity.Notifications.Installer", "src\Unity.Notifications.Installer\Unity.Notifications.Installer.csproj", "{BE39FD00-745B-4049-8161-FC129817CBE4}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/Unity.Payments.sln b/applications/Unity.GrantManager/modules/Unity.Payments/Unity.Payments.sln index 235b0b5753..59281f42db 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/Unity.Payments.sln +++ b/applications/Unity.GrantManager/modules/Unity.Payments/Unity.Payments.sln @@ -35,7 +35,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unity.Payments.Web", "src\U EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unity.Payments.HttpApi.Client.ConsoleTestApp", "test\Unity.Payments.HttpApi.Client.ConsoleTestApp\Unity.Payments.HttpApi.Client.ConsoleTestApp.csproj", "{1EDCD6D4-DF3A-4E3B-ABB6-C0D0B373EAB8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.Payments.Installer", "src\Unity.Payments.Installer\Unity.Payments.Installer.csproj", "{BE39FD00-745B-4049-8161-FC129817CBE4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unity.Payments.Installer", "src\Unity.Payments.Installer\Unity.Payments.Installer.csproj", "{BE39FD00-745B-4049-8161-FC129817CBE4}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/Unity.Reporting.sln b/applications/Unity.GrantManager/modules/Unity.Reporting/Unity.Reporting.sln index 0bbebbee35..c14d0cca23 100644 --- a/applications/Unity.GrantManager/modules/Unity.Reporting/Unity.Reporting.sln +++ b/applications/Unity.GrantManager/modules/Unity.Reporting/Unity.Reporting.sln @@ -35,7 +35,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unity.Reporting.Web", "src\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unity.Reporting.HttpApi.Client.ConsoleTestApp", "test\Unity.Reporting.HttpApi.Client.ConsoleTestApp\Unity.Reporting.HttpApi.Client.ConsoleTestApp.csproj", "{1EDCD6D4-DF3A-4E3B-ABB6-C0D0B373EAB8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.Reporting.Installer", "src\Unity.Reporting.Installer\Unity.Reporting.Installer.csproj", "{BE39FD00-745B-4049-8161-FC129817CBE4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unity.Reporting.Installer", "src\Unity.Reporting.Installer\Unity.Reporting.Installer.csproj", "{BE39FD00-745B-4049-8161-FC129817CBE4}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Assessments/AssessmentAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Assessments/AssessmentAppService.cs index 95b07c4845..4cbc3f3ff4 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Assessments/AssessmentAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Assessments/AssessmentAppService.cs @@ -14,12 +14,10 @@ using Unity.GrantManager.Applications; using Unity.GrantManager.Comments; using Unity.GrantManager.Exceptions; -using Unity.GrantManager.Permissions; using Unity.GrantManager.Workflow; using Unity.Modules.Shared; using Volo.Abp; using Volo.Abp.Application.Services; -using Volo.Abp.Authorization.Permissions; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Repositories; using Volo.Abp.EventBus.Local; @@ -96,7 +94,7 @@ public async Task GetDisplayList(Guid applicationId) // If AI Scoring feature is disabled, or user doesn't have permissions to view AI assessments, filter out AI assessments from the list var aiScoringEnabled = await _featureChecker.IsEnabledAsync("Unity.AI.Scoring"); - var canViewAI = await AuthorizationService.IsGrantedAsync(AIPermissions.Default.ScoringAssistant.Default); + var canViewAI = await AuthorizationService.IsGrantedAsync(AIPermissions.ScoringAssistant.ScoringAssistantDefault); assessmentList = assessmentList .Where(a => !a.IsAiAssessment || (aiScoringEnabled && canViewAI)) .OrderByDescending(a => a.IsAiAssessment) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenuContributor.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenuContributor.cs index 6233d6ca6e..f5fe33bf96 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenuContributor.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenuContributor.cs @@ -125,7 +125,7 @@ private async static Task ConfigureMainMenuAsync(MenuConfigurationContext contex l["Menu:AIReporting"], "~/AIReporting", icon: "fl fl-view-dashboard", - requiredPermissionName: AIPermissions.Default.Reporting.Default, + requiredPermissionName: AIPermissions.Reporting.ReportingDefault, order: 9 ) ); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml index d3a22c44b2..96441a8cce 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml @@ -32,7 +32,7 @@ var notificationsFeatureEnabled = await FeatureChecker.IsEnabledAsync("Unity.Notifications"); var readEmailGranted = await PermissionChecker.IsGrantedAsync("Notifications.Email"); var aiApplicationAnalysisEnabled = await FeatureChecker.IsEnabledAsync("Unity.AI.ApplicationAnalysis") - && await PermissionChecker.IsGrantedAsync(AIPermissions.Default.ApplicationAnalysis.Default); + && await PermissionChecker.IsGrantedAsync(AIPermissions.ApplicationAnalysis.ApplicationAnalysisDefault); var flexFeatureEnabled = await FeatureChecker.IsEnabledAsync("Unity.Flex"); } @section styles diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.cs index 001cf10a62..066f5cc80a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.cs @@ -30,7 +30,7 @@ public async Task InvokeAsync() { var isAIAttachmentSummariesEnabled = await _featureChecker.IsEnabledAsync("Unity.AI.AttachmentSummaries") && - await _permissionChecker.IsGrantedAsync(AIPermissions.Default.AttachmentSummary.Default); + await _permissionChecker.IsGrantedAsync(AIPermissions.AttachmentSummary.AttachmentSummaryDefault); ViewBag.IsAIAttachmentSummariesEnabled = isAIAttachmentSummariesEnabled; return View(); } From 931be943c58989c82831246c446e2d779c992e7f Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Fri, 6 Mar 2026 14:57:59 -0800 Subject: [PATCH 05/58] feature/AB#32216-BuildAIBase-Permission:SonarFixes --- .../Unity.AI/src/Unity.AI.Domain.Shared/Localization/AI/en.json | 1 - .../Configuration/FieldsProviders/ScoresheetFieldsProvider.cs | 2 +- .../GrantApplicationPermissionDefinitionProvider.cs | 1 - .../Pages/GrantApplications/Details.cshtml | 1 - .../Shared/Components/ChefsAttachments/ChefsAttachments.cs | 1 - 5 files changed, 1 insertion(+), 5 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AI/en.json b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AI/en.json index 775b9a4d87..f660d259d3 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AI/en.json +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AI/en.json @@ -2,7 +2,6 @@ "culture": "en", "texts": { "Permission:AI": "AI", - "Permission:AI.Default": "AI Management", "Permission:AI.Reporting": "AI Reporting", "Permission:AI.ApplicationAnalysis": "AI Application Analysis", "Permission:AI.AttachmentSummary": "AI Attachment Summary", diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/ScoresheetFieldsProvider.cs b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/ScoresheetFieldsProvider.cs index 4b856b3af3..7372d461f9 100644 --- a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/ScoresheetFieldsProvider.cs +++ b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/ScoresheetFieldsProvider.cs @@ -233,7 +233,7 @@ private sealed class ScoresheetMapping /// Gets or sets the metadata information associated with the mapping. /// Contains contextual information about scoresheets and other correlation-specific details. /// - public MapMetadataDto? Metadata { get; set; } + public MapMetadataDto? Metadata { get; set; } = new MapMetadataDto(); } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantApplications/GrantApplicationPermissionDefinitionProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantApplications/GrantApplicationPermissionDefinitionProvider.cs index d21954da46..5dae52cf3e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantApplications/GrantApplicationPermissionDefinitionProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantApplications/GrantApplicationPermissionDefinitionProvider.cs @@ -1,7 +1,6 @@ using Unity.GrantManager.Localization; using Unity.Modules.Shared; using Volo.Abp.Authorization.Permissions; -using Volo.Abp.Features; using Volo.Abp.Localization; using Volo.Abp.SettingManagement; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml index 96441a8cce..f00fa21704 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml @@ -5,7 +5,6 @@ @using Unity.Flex.Web.Views.Shared.Components.WorksheetInstanceWidget @using Unity.GrantManager.Flex @using Unity.GrantManager.Localization -@using Unity.GrantManager.Permissions @using Unity.GrantManager.Web.Pages.GrantApplications @using Unity.GrantManager.Web.Views.Shared.Components.CustomTabWidget @using Unity.GrantManager.Web.Views.Shared.Components.DetailsActionBar diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.cs index 066f5cc80a..530a0338fd 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using Volo.Abp.Features; using Volo.Abp.Authorization.Permissions; -using Unity.GrantManager.Permissions; using Unity.AI.Permissions; namespace Unity.GrantManager.Web.Views.Shared.Components.ChefsAttachments From 4ab223f12c4e715ddd906204e1bd047b5af15b72 Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Fri, 6 Mar 2026 15:24:36 -0800 Subject: [PATCH 06/58] feature/AB#32216-BuildAIBase-Permission:SonarFixes --- .../modules/Unity.AI/src/Unity.AI.Web/Unity.AI.Web.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Unity.AI.Web.csproj b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Unity.AI.Web.csproj index b1233b386d..79de5268e6 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Unity.AI.Web.csproj +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Unity.AI.Web.csproj @@ -8,7 +8,6 @@ true Library Unity.AI.Web - true From 9667a1058e97091b6320845a94ecd0542715247a Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 6 Mar 2026 16:11:12 -0800 Subject: [PATCH 07/58] AB#32009 Add prompt baseline snapshots for v0 and v1 comparison --- .../Baselines/v0/AnalysisPrompts.v1.txt | 58 + .../Baselines/v0/AttachmentPrompts.v1.txt | 29 + .../Baselines/v0/OpenAIService.v0.cs.txt | 406 ++++++ .../Baselines/v0/OpenAIService.v1.cs.txt | 1134 +++++++++++++++++ .../Baselines/v0/PromptCoreRules.v1.txt | 13 + .../Prompts/Baselines/v0/PromptHeader.v1.txt | 14 + .../Baselines/v0/ScoresheetPrompts.v1.txt | 85 ++ 7 files changed, 1739 insertions(+) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/AnalysisPrompts.v1.txt create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/AttachmentPrompts.v1.txt create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/OpenAIService.v0.cs.txt create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/OpenAIService.v1.cs.txt create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/PromptCoreRules.v1.txt create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/PromptHeader.v1.txt create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/ScoresheetPrompts.v1.txt diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/AnalysisPrompts.v1.txt b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/AnalysisPrompts.v1.txt new file mode 100644 index 0000000000..d267a1216e --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/AnalysisPrompts.v1.txt @@ -0,0 +1,58 @@ +namespace Unity.GrantManager.AI +{ + internal static class AnalysisPrompts + { + public const string DefaultRubric = @"ELIGIBILITY REQUIREMENTS: Project aligns with program objectives; Applicant is an eligible entity; Budget is reasonable and justified; Timeline is realistic. +COMPLETENESS CHECKS: Required information is present; Supporting materials are provided where applicable; Description is clear. +FINANCIAL REVIEW: Requested amount is within limits; Budget matches scope; Matching funds or contributions are identified. +RISK ASSESSMENT: Applicant capacity; Feasibility; Compliance considerations; Delivery risks. +QUALITY INDICATORS: Clear objectives; Defined beneficiaries; Appropriate approach; Long-term sustainability."; + + public const string ScoreRules = @"HIGH: Application demonstrates strong evidence across most rubric areas with few or no issues. +MEDIUM: Application has some gaps or weaknesses that require reviewer attention. +LOW: Application has significant gaps or risks across key rubric areas."; + + public const string OutputTemplate = @"{ + ""rating"": """", + ""errors"": [ + { + ""title"": """", + ""detail"": """" + } + ], + ""warnings"": [ + { + ""title"": """", + ""detail"": """" + } + ], + ""summaries"": [ + { + ""title"": """", + ""detail"": """" + } + ] +}"; + + public const string Rules = PromptCoreRules.UseProvidedEvidence + "\n" + + "- Do not invent fields, documents, requirements, or facts.\n" + + @"- Treat missing or empty values as findings only when they weaken rubric evidence. +- Prefer material issues; avoid nitpicking. +- Use 3-6 words for title. +- Each detail must be 1-2 complete sentences. +- Each detail must cite concrete evidence from DATA or ATTACHMENTS. +- If ATTACHMENTS evidence is used, cite the attachment by name in detail. +- If no findings exist, return empty arrays. +- Rating must be HIGH, MEDIUM, or LOW. +" + + PromptCoreRules.MinimumNarrativeWords + "\n" + + PromptCoreRules.ExactOutputShape + "\n" + + PromptCoreRules.NoExtraOutputKeys + "\n" + + PromptCoreRules.ValidJsonOnly + "\n" + + PromptCoreRules.PlainJsonOnly; + + public static readonly string SystemPrompt = PromptHeader.Build( + "You are an expert grant analyst assistant for human reviewers.", + "Using SCHEMA, DATA, ATTACHMENTS, RUBRIC, SCORE, OUTPUT, and RULES, return review findings."); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/AttachmentPrompts.v1.txt b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/AttachmentPrompts.v1.txt new file mode 100644 index 0000000000..a61cc50848 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/AttachmentPrompts.v1.txt @@ -0,0 +1,29 @@ +namespace Unity.GrantManager.AI +{ + internal static class AttachmentPrompts + { + public static readonly string SystemPrompt = PromptHeader.Build( + "You are a professional grant analyst for the BC Government.", + "Produce a concise reviewer-facing summary of the provided attachment context."); + + public const string OutputSection = @"OUTPUT +{ + ""summary"": """" +}"; + + public const string RulesSection = "- Use only ATTACHMENT as evidence.\n" + + "- If ATTACHMENT.text is present, summarize actual content.\n" + + "- If ATTACHMENT.text is null or empty, provide a conservative file-level summary.\n" + + PromptCoreRules.NoInvention + "\n" + + @"- Write 1-2 complete sentences. +- Summary must be grounded in concrete ATTACHMENT evidence. +- Return exactly one object with only the key: summary. +" + + PromptCoreRules.MinimumNarrativeWords + "\n" + + PromptCoreRules.ExactOutputShape + "\n" + + PromptCoreRules.NoExtraOutputKeys + "\n" + + PromptCoreRules.ValidJsonOnly + "\n" + + PromptCoreRules.PlainJsonOnly; + } +} + diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/OpenAIService.v0.cs.txt b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/OpenAIService.v0.cs.txt new file mode 100644 index 0000000000..239db10623 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/OpenAIService.v0.cs.txt @@ -0,0 +1,406 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace Unity.GrantManager.AI +{ + public class OpenAIService : IAIService, ITransientDependency + { + private readonly HttpClient _httpClient; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + private readonly ITextExtractionService _textExtractionService; + + private string? ApiKey => _configuration["AI:OpenAI:ApiKey"]; + private string? ApiUrl => _configuration["AI:OpenAI:ApiUrl"] ?? "https://api.openai.com/v1/chat/completions"; + private readonly string NoKeyError = "OpenAI API key is not configured"; + + public OpenAIService(HttpClient httpClient, IConfiguration configuration, ILogger logger, ITextExtractionService textExtractionService) + { + _httpClient = httpClient; + _configuration = configuration; + _logger = logger; + _textExtractionService = textExtractionService; + } + + public Task IsAvailableAsync() + { + if (string.IsNullOrEmpty(ApiKey)) + { + _logger.LogWarning("Error: {Message}", NoKeyError); + return Task.FromResult(false); + } + + return Task.FromResult(true); + } + + public async Task GenerateSummaryAsync(string content, string? prompt = null, int maxTokens = 150) + { + if (string.IsNullOrEmpty(ApiKey)) + { + _logger.LogWarning("Error: {Message}", NoKeyError); + return "AI analysis not available - service not configured."; + } + + _logger.LogDebug("Calling OpenAI with prompt: {Prompt}", content); + + try + { + var systemPrompt = prompt ?? "You are a professional grant analyst for the BC Government."; + + var requestBody = new + { + messages = new[] + { + new { role = "system", content = systemPrompt }, + new { role = "user", content = content } + }, + max_tokens = maxTokens, + temperature = 0.3 + }; + + var json = JsonSerializer.Serialize(requestBody); + var httpContent = new StringContent(json, Encoding.UTF8, "application/json"); + + _httpClient.DefaultRequestHeaders.Clear(); + _httpClient.DefaultRequestHeaders.Add("Authorization", ApiKey); + + var response = await _httpClient.PostAsync(ApiUrl, httpContent); + var responseContent = await response.Content.ReadAsStringAsync(); + + _logger.LogDebug("Response: {Response}", responseContent); + + if (!response.IsSuccessStatusCode) + { + _logger.LogError("OpenAI API request failed: {StatusCode} - {Content}", response.StatusCode, responseContent); + return "AI analysis failed - service temporarily unavailable."; + } + + using var jsonDoc = JsonDocument.Parse(responseContent); + var choices = jsonDoc.RootElement.GetProperty("choices"); + if (choices.GetArrayLength() > 0) + { + var message = choices[0].GetProperty("message"); + return message.GetProperty("content").GetString() ?? "No summary generated."; + } + + return "No summary generated."; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error generating AI summary"); + return "AI analysis failed - please try again later."; + } + } + + public async Task GenerateAttachmentSummaryAsync(string fileName, byte[] fileContent, string contentType) + { + try + { + var extractedText = await _textExtractionService.ExtractTextAsync(fileName, fileContent, contentType); + + string contentToAnalyze; + string prompt; + + if (!string.IsNullOrWhiteSpace(extractedText)) + { + _logger.LogDebug("Extracted {TextLength} characters from {FileName}", extractedText.Length, fileName); + + contentToAnalyze = $"Document: {fileName}\nType: {contentType}\nContent:\n{extractedText}"; + prompt = "Please analyze this document and provide a concise summary of its content, purpose, and key information, for use by your fellow grant analysts. It should be 1-2 sentences long and about 46 tokens."; + } + else + { + _logger.LogDebug("No text extracted from {FileName}, analyzing metadata only", fileName); + + contentToAnalyze = $"File: {fileName}, Type: {contentType}, Size: {fileContent.Length} bytes"; + prompt = "Please analyze this document and provide a concise summary of its content, purpose, and key information, for use by your fellow grant analysts. It should be 1-2 sentences long and about 46 tokens."; + } + + return await GenerateSummaryAsync(contentToAnalyze, prompt, 150); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error generating attachment summary for {FileName}", fileName); + return $"AI analysis not available for this attachment ({fileName})."; + } + } + + public async Task AnalyzeApplicationAsync(string applicationContent, List attachmentSummaries, string rubric, string? formFieldConfiguration = null) + { + if (string.IsNullOrEmpty(ApiKey)) + { + _logger.LogWarning("{Message}", NoKeyError); + return "AI analysis not available - service not configured."; + } + + try + { + var attachmentSummariesText = attachmentSummaries?.Count > 0 + ? string.Join("\n- ", attachmentSummaries.Select((s, i) => $"Attachment {i + 1}: {s}")) + : "No attachments provided."; + + var fieldConfigurationSection = !string.IsNullOrEmpty(formFieldConfiguration) + ? $@" +{formFieldConfiguration}" + : string.Empty; + + var analysisContent = $@"APPLICATION CONTENT: +{applicationContent} + +ATTACHMENT SUMMARIES: +- {attachmentSummariesText} +{fieldConfigurationSection} + +EVALUATION RUBRIC: +{rubric} + +Analyze this grant application comprehensively across all five rubric categories (Eligibility, Completeness, Financial Review, Risk Assessment, and Quality Indicators). Identify issues, concerns, and areas for improvement. Return your findings in the following JSON format: +{{ + ""overall_score"": ""HIGH/MEDIUM/LOW"", + ""warnings"": [ + {{ + ""category"": ""Brief summary of the warning"", + ""message"": ""Detailed warning message with full context and explanation"", + ""severity"": ""WARNING"" + }} + ], + ""errors"": [ + {{ + ""category"": ""Brief summary of the error"", + ""message"": ""Detailed error message with full context and explanation"", + ""severity"": ""ERROR"" + }} + ], + ""recommendations"": [ + {{ + ""category"": ""Brief summary of the recommendation"", + ""message"": ""Detailed recommendation with specific actionable guidance"" + }} + ] +}} + +Important: The 'category' field should be a concise summary (3-6 words) that captures the essence of the issue, while the 'message' field should contain the detailed explanation."; + + var systemPrompt = @"You are an expert grant application reviewer for the BC Government. + +Conduct a thorough, comprehensive analysis across all rubric categories. Identify substantive issues, concerns, and opportunities for improvement. + +Classify findings based on their impact on the application's evaluation and fundability: +- ERRORS: Important missing information, significant gaps in required content, compliance issues, or major concerns affecting eligibility +- WARNINGS: Areas needing clarification, moderate issues, or concerns that should be addressed + +Evaluate the quality, clarity, and appropriateness of all application content. Be thorough but fair - identify real issues while avoiding nitpicking. + +Respond only with valid JSON in the exact format requested."; + + var rawAnalysis = await GenerateSummaryAsync(analysisContent, systemPrompt, 1000); + + // Post-process the AI response to add unique IDs to errors and warnings + return AddIdsToAnalysisItems(rawAnalysis); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error analyzing application"); + return "AI analysis failed - please try again later."; + } + } + + private string AddIdsToAnalysisItems(string analysisJson) + { + try + { + using var jsonDoc = JsonDocument.Parse(analysisJson); + using var memoryStream = new System.IO.MemoryStream(); + using (var writer = new Utf8JsonWriter(memoryStream, new JsonWriterOptions { Indented = true })) + { + writer.WriteStartObject(); + + foreach (var property in jsonDoc.RootElement.EnumerateObject()) + { + if (property.Name == "errors" || property.Name == "warnings") + { + writer.WritePropertyName(property.Name); + writer.WriteStartArray(); + + foreach (var item in property.Value.EnumerateArray()) + { + writer.WriteStartObject(); + + // Add unique ID first + writer.WriteString("id", Guid.NewGuid().ToString()); + + // Copy existing properties + foreach (var itemProperty in item.EnumerateObject()) + { + itemProperty.WriteTo(writer); + } + + writer.WriteEndObject(); + } + + writer.WriteEndArray(); + } + else + { + property.WriteTo(writer); + } + } + + // Add dismissed_items array if not present + if (!jsonDoc.RootElement.TryGetProperty("dismissed_items", out _)) + { + writer.WritePropertyName("dismissed_items"); + writer.WriteStartArray(); + writer.WriteEndArray(); + } + + writer.WriteEndObject(); + } + + return Encoding.UTF8.GetString(memoryStream.ToArray()); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error adding IDs to analysis items, returning original JSON"); + return analysisJson; // Return original if processing fails + } + } + + public async Task GenerateScoresheetAnswersAsync(string applicationContent, List attachmentSummaries, string scoresheetQuestions) + { + if (string.IsNullOrEmpty(ApiKey)) + { + _logger.LogWarning("{Message}", NoKeyError); + return "{}"; + } + + try + { + var attachmentSummariesText = attachmentSummaries?.Count > 0 + ? string.Join("\n- ", attachmentSummaries.Select((s, i) => $"Attachment {i + 1}: {s}")) + : "No attachments provided."; + + var analysisContent = $@"APPLICATION CONTENT: +{applicationContent} + +ATTACHMENT SUMMARIES: +- {attachmentSummariesText} + +SCORESHEET QUESTIONS: +{scoresheetQuestions} + +Please analyze this grant application and provide appropriate answers for each scoresheet question. + +For numeric questions, provide a numeric value within the specified range. +For yes/no questions, provide either 'Yes' or 'No'. +For text questions, provide a concise, relevant response. +For select list questions, choose the most appropriate option from the provided choices. +For text area questions, provide a detailed but concise response. + +Base your answers on the application content and attachment summaries provided. Be objective and fair in your assessment. + +Return your response as a JSON object where each key is the question ID and the value is the appropriate answer: +{{ + ""question-id-1"": ""answer-value-1"", + ""question-id-2"": ""answer-value-2"" +}} +Do not return any markdown formatting, just the JSON by itself"; + + var systemPrompt = @"You are an expert grant application reviewer for the BC Government. +Analyze the provided application and generate appropriate answers for the scoresheet questions based on the application content. +Be thorough, objective, and fair in your assessment. Base your answers strictly on the provided application content. +Respond only with valid JSON in the exact format requested."; + + return await GenerateSummaryAsync(analysisContent, systemPrompt, 2000); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error generating scoresheet answers"); + return "{}"; + } + } + + public async Task GenerateScoresheetSectionAnswersAsync(string applicationContent, List attachmentSummaries, string sectionJson, string sectionName) + { + if (string.IsNullOrEmpty(ApiKey)) + { + _logger.LogWarning("{Message}", NoKeyError); + return "{}"; + } + + try + { + var attachmentSummariesText = attachmentSummaries?.Count > 0 + ? string.Join("\n- ", attachmentSummaries.Select((s, i) => $"Attachment {i + 1}: {s}")) + : "No attachments provided."; + + var analysisContent = $@"APPLICATION CONTENT: +{applicationContent} + +ATTACHMENT SUMMARIES: +- {attachmentSummariesText} + +SCORESHEET SECTION: {sectionName} +{sectionJson} + +Please analyze this grant application and provide appropriate answers for each question in the ""{sectionName}"" section only. + +For each question, provide: +1. Your answer based on the application content +2. A brief cited description (1-2 sentences) explaining your reasoning with specific references to the application content +3. A confidence score from 0-100 indicating how confident you are in your answer based on available information + +Guidelines for answers: +- For numeric questions, provide a numeric value within the specified range +- For yes/no questions, provide either 'Yes' or 'No' +- For text questions, provide a concise, relevant response +- For select list questions, respond with ONLY the number from the 'number' field (1, 2, 3, etc.) of your chosen option. NEVER return 0 - the lowest valid answer is 1. For example: if you want '(0 pts) No outcomes provided', choose the option where number=1, not 0. +- For text area questions, provide a detailed but concise response +- Base your confidence score on how clearly the application content supports your answer + +Return your response as a JSON object where each key is the question ID and the value contains the answer, citation, and confidence: +{{ + ""question-id-1"": {{ + ""answer"": ""your-answer-here"", + ""citation"": ""Brief explanation with specific reference to application content"", + ""confidence"": 85 + }}, + ""question-id-2"": {{ + ""answer"": ""3"", + ""citation"": ""Based on the project budget of $50,000 mentioned in the application, this falls into the medium budget category"", + ""confidence"": 90 + }} +}} + +IMPORTANT FOR SELECT LIST QUESTIONS: If a question has availableOptions like: +[{{""number"":1,""value"":""Low (Under $25K)""}}, {{""number"":2,""value"":""Medium ($25K-$75K)""}}, {{""number"":3,""value"":""High (Over $75K)""}}] +Then respond with ONLY the number (e.g., ""3"" for ""High (Over $75K)""), not the text value. + +Do not return any markdown formatting, just the JSON by itself"; + + var systemPrompt = @"You are an expert grant application reviewer for the BC Government. +Analyze the provided application and generate appropriate answers for the scoresheet section questions based on the application content. +Be thorough, objective, and fair in your assessment. Base your answers strictly on the provided application content. +Always provide citations that reference specific parts of the application content to support your answers. +Be honest about your confidence level - if information is missing or unclear, reflect this in a lower confidence score. +Respond only with valid JSON in the exact format requested."; + + return await GenerateSummaryAsync(analysisContent, systemPrompt, 2000); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error generating scoresheet section answers for section {SectionName}", sectionName); + return "{}"; + } + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/OpenAIService.v1.cs.txt b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/OpenAIService.v1.cs.txt new file mode 100644 index 0000000000..e421036ebd --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/OpenAIService.v1.cs.txt @@ -0,0 +1,1134 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace Unity.GrantManager.AI +{ + public class OpenAIService : IAIService, ITransientDependency + { + private readonly HttpClient _httpClient; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + private readonly ITextExtractionService _textExtractionService; + private readonly JsonSerializerOptions _prettyJsonOptions = new() { WriteIndented = true }; + + private string? ApiKey => _configuration["AI:OpenAI:ApiKey"]; + private bool HasApiKey => !string.IsNullOrWhiteSpace(ApiKey); + private string? ApiUrl => _configuration["AI:OpenAI:ApiUrl"] ?? "https://api.openai.com/v1/chat/completions"; + private bool LogPayloads => _configuration.GetValue("AI:Logging:LogPayloads") ?? false; + private readonly string NoApiKeyMessage = "OpenAI API key is not configured"; + private const string ServiceNotConfiguredMessage = "AI analysis not available - service not configured."; + private const string ServiceTemporarilyUnavailableMessage = "AI analysis failed - service temporarily unavailable."; + private const string GenericFailureMessage = "AI analysis failed - please try again later."; + private const string NoSummaryGeneratedMessage = "No summary generated."; + private const string EmptyJsonObject = "{}"; + private const int ScoresheetAllMaxTokens = 2000; + private const int ScoresheetSectionMaxTokens = 3200; + private const int AnalysisMaxTokens = 1600; + private const string DefaultContentType = "application/octet-stream"; + private const int AttachmentSummaryMaxTokens = 240; + private const string AttachmentSummaryUnavailableMessage = "AI analysis not available for this attachment"; + private const string ScoreHigh = "HIGH"; + private const string ScoreMedium = "MEDIUM"; + private const string ScoreLow = "LOW"; + private const string AiPromptLogRelativePath = "logs/ai-prompts.log"; + private static int _aiPromptLogInitialized; + + public OpenAIService(HttpClient httpClient, IConfiguration configuration, ILogger logger, ITextExtractionService textExtractionService) + { + _httpClient = httpClient; + _configuration = configuration; + _logger = logger; + _textExtractionService = textExtractionService; + } + + public Task IsAvailableAsync() + { + if (!HasApiKey) + { + _logger.LogWarning("Error: {Message}", NoApiKeyMessage); + return Task.FromResult(false); + } + + return Task.FromResult(true); + } + + public async Task GenerateCompletionAsync(AICompletionRequest request) + { + if (request == null) + { + _logger.LogWarning("AI completion request was null."); + return GenericFailureMessage; + } + + var userPrompt = request.UserPrompt ?? string.Empty; + var systemPrompt = request.SystemPrompt; + var maxTokens = request.MaxTokens <= 0 ? 150 : request.MaxTokens; + var temperature = request.Temperature ?? 0.3; + + return await ExecuteChatCompletionAsync(userPrompt, systemPrompt, maxTokens, temperature); + } + + private async Task ExecuteChatCompletionAsync( + string userPrompt, + string? systemPrompt = null, + int maxTokens = 150, + double temperature = 0.3) + { + if (!HasApiKey) + { + _logger.LogWarning("Error: {Message}", NoApiKeyMessage); + return ServiceNotConfiguredMessage; + } + + _logger.LogDebug( + "Calling OpenAI chat completions. PromptLength: {PromptLength}, MaxTokens: {MaxTokens}", + userPrompt?.Length ?? 0, + maxTokens); + + try + { + string resolvedSystemPrompt = systemPrompt ?? "You are a professional grant analyst for the BC Government."; + + var requestBody = new Dictionary + { + ["messages"] = new[] + { + new { role = "system", content = resolvedSystemPrompt }, + new { role = "user", content = userPrompt ?? string.Empty } + }, + ["max_tokens"] = maxTokens, + ["temperature"] = temperature + }; + + var json = JsonSerializer.Serialize(requestBody); + var authValue = ApiKey!.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase) + ? ApiKey.Substring("Bearer ".Length).Trim() + : ApiKey; + + using var request = new HttpRequestMessage(HttpMethod.Post, ApiUrl); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authValue); + request.Content = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await _httpClient.SendAsync(request); + var responseContent = await response.Content.ReadAsStringAsync() ?? string.Empty; + + _logger.LogDebug( + "OpenAI chat completions response received. StatusCode: {StatusCode}, ResponseLength: {ResponseLength}", + response.StatusCode, + responseContent?.Length ?? 0); + + if (!response.IsSuccessStatusCode) + { + _logger.LogError("OpenAI API request failed: {StatusCode} - {Content}", response.StatusCode, responseContent); + return ServiceTemporarilyUnavailableMessage; + } + + using var jsonDoc = JsonDocument.Parse(responseContent ?? string.Empty); + var choices = jsonDoc.RootElement.GetProperty("choices"); + if (choices.GetArrayLength() > 0) + { + var message = choices[0].GetProperty("message"); + return message.GetProperty("content").GetString() ?? NoSummaryGeneratedMessage; + } + + return NoSummaryGeneratedMessage; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error generating AI summary"); + return GenericFailureMessage; + } + } + + // Canonical attachment summary prompt contract is defined by: + // AttachmentPrompts.SystemPrompt, AttachmentPrompts.OutputSection, and AttachmentPrompts.RulesSection. + public async Task GenerateAttachmentSummaryAsync(AttachmentSummaryRequest request) + { + try + { + if (request == null) + { + _logger.LogWarning("Attachment summary request was null."); + return $"{AttachmentSummaryUnavailableMessage} (unknown)."; + } + + var normalizedFileName = string.IsNullOrWhiteSpace(request.FileName) ? "unknown" : request.FileName.Trim(); + var normalizedContentType = string.IsNullOrWhiteSpace(request.ContentType) ? DefaultContentType : request.ContentType.Trim(); + var normalizedFileContent = request.FileContent ?? Array.Empty(); + var extractedText = await _textExtractionService.ExtractTextAsync(normalizedFileName, normalizedFileContent, normalizedContentType); + + var hasExtractedText = !string.IsNullOrWhiteSpace(extractedText); + if (hasExtractedText) + { + _logger.LogDebug("Extracted {TextLength} characters from {FileName}", extractedText!.Length, normalizedFileName); + } + else + { + _logger.LogDebug("No text extracted from {FileName}, analyzing metadata only", normalizedFileName); + } + + var attachmentInput = new AttachmentPromptInput + { + Name = normalizedFileName, + Text = hasExtractedText ? extractedText : null + }; + + var contentToAnalyze = BuildAttachmentSummaryPrompt(attachmentInput); + return await ExecutePromptWithRetryAsync( + promptType: "AttachmentSummary", + systemPrompt: AttachmentPrompts.SystemPrompt, + userPrompt: contentToAnalyze, + maxTokens: AttachmentSummaryMaxTokens, + normalizeResponse: NormalizeAttachmentSummaryResponse, + isValidNormalizedResponse: normalized => !string.IsNullOrWhiteSpace(normalized), + fallbackResponse: string.Empty); + } + catch (Exception ex) + { + var fileName = request?.FileName ?? "unknown"; + _logger.LogError(ex, "Error generating attachment summary for {FileName}", fileName); + return $"{AttachmentSummaryUnavailableMessage} ({fileName})."; + } + } + + private string BuildAttachmentSummaryPrompt(AttachmentPromptInput attachmentInput) + { + return $@"ATTACHMENT +{JsonSerializer.Serialize(attachmentInput, _prettyJsonOptions)} + +{AttachmentPrompts.OutputSection} + +RULES +{AttachmentPrompts.RulesSection}"; + } + + private string NormalizeAttachmentSummaryResponse(string response) + { + if (!TryParseJsonObjectFromResponse(response, out var responseObject)) + { + return string.Empty; + } + + if (responseObject.TryGetProperty(AIJsonKeys.Summary, out var summaryProp) && + summaryProp.ValueKind == JsonValueKind.String) + { + return summaryProp.GetString()?.Trim() ?? string.Empty; + } + + return responseObject.ToString().Trim(); + } + + // Canonical analysis prompt contract is defined by: + // AnalysisPrompts.DefaultRubric, AnalysisPrompts.ScoreRules, AnalysisPrompts.OutputTemplate, + // AnalysisPrompts.Rules, and AnalysisPrompts.SystemPrompt. + public async Task GenerateApplicationAnalysisAsync(ApplicationAnalysisRequest request) + { + if (!HasApiKey) + { + _logger.LogWarning("{Message}", NoApiKeyMessage); + return ServiceNotConfiguredMessage; + } + + try + { + if (request == null) + { + _logger.LogWarning("Application analysis request was null."); + return BuildEmptyAnalysisResponseJson(); + } + + var emptyObject = CreateEmptyJsonObject(); + var schemaPayload = request.Schema.ValueKind == JsonValueKind.Undefined ? emptyObject : request.Schema; + var dataPayload = request.Data.ValueKind == JsonValueKind.Undefined ? emptyObject : request.Data; + + if (schemaPayload.ValueKind != JsonValueKind.Object || dataPayload.ValueKind != JsonValueKind.Object) + { + _logger.LogWarning( + "Invalid application analysis request payload shape. Schema kind: {SchemaKind}, Data kind: {DataKind}.", + schemaPayload.ValueKind, + dataPayload.ValueKind); + return BuildEmptyAnalysisResponseJson(); + } + + var attachmentsPayload = request.Attachments? + .Where(a => a != null && !string.IsNullOrWhiteSpace(a.Summary)) + .Select(a => new ApplicationAnalysisAttachment + { + Name = string.IsNullOrWhiteSpace(a.Name) ? "attachment" : a.Name.Trim(), + Summary = a.Summary.Trim() + }) + .ToList() ?? new List(); + + var rubricText = !string.IsNullOrWhiteSpace(request.Rubric) ? request.Rubric : AnalysisPrompts.DefaultRubric; + var analysisPrompt = BuildAnalysisPrompt(schemaPayload, dataPayload, attachmentsPayload, rubricText); + return await ExecutePromptWithRetryAsync( + promptType: "ApplicationAnalysis", + systemPrompt: AnalysisPrompts.SystemPrompt, + userPrompt: analysisPrompt, + maxTokens: AnalysisMaxTokens, + normalizeResponse: NormalizeAnalysisResponse, + isValidNormalizedResponse: IsValidAnalysisNormalizedResponse, + fallbackResponse: BuildEmptyAnalysisResponseJson()); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error analyzing application"); + return BuildEmptyAnalysisResponseJson(); + } + } + + private string NormalizeAnalysisResponse(string analysisJson) + { + try + { + if (!TryParseJsonObjectFromResponse(analysisJson, out var analysisObject)) + { + _logger.LogError("Invalid analysis JSON response."); + return BuildEmptyAnalysisResponseJson(); + } + + var parseOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + + var model = JsonSerializer.Deserialize(analysisObject.GetRawText(), parseOptions); + if (model == null) + { + return BuildEmptyAnalysisResponseJson(); + } + + model.Errors ??= new List(); + model.Warnings ??= new List(); + model.Summaries ??= new List(); + model.Dismissed ??= new List(); + + model.Rating = NormalizeRating(model.Rating); + + foreach (var error in model.Errors) + { + error.Id = string.IsNullOrWhiteSpace(error.Id) ? Guid.NewGuid().ToString() : error.Id; + } + + foreach (var warning in model.Warnings) + { + warning.Id = string.IsNullOrWhiteSpace(warning.Id) ? Guid.NewGuid().ToString() : warning.Id; + } + + model.Dismissed = model.Dismissed + .Where(id => !string.IsNullOrWhiteSpace(id)) + .Distinct(StringComparer.Ordinal) + .ToList(); + + var normalizedOutput = new ApplicationAnalysisResponse + { + Rating = model.Rating, + Errors = model.Errors, + Warnings = model.Warnings, + Summaries = model.Summaries, + Dismissed = model.Dismissed + }; + + return JsonSerializer.Serialize(normalizedOutput, _prettyJsonOptions); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error normalizing analysis response."); + return BuildEmptyAnalysisResponseJson(); + } + } + + private static string NormalizeRating(string? rating) + { + var normalized = rating?.Trim().ToUpperInvariant(); + return normalized switch + { + ScoreHigh => ScoreHigh, + ScoreMedium => ScoreMedium, + ScoreLow => ScoreLow, + _ => ScoreMedium + }; + } + + private string BuildAnalysisPrompt( + JsonElement schemaPayload, + JsonElement dataPayload, + List attachmentsPayload, + string rubricText) + { + var analysisAttachments = (attachmentsPayload ?? new List()) + .Select(a => new AnalysisAttachmentPromptItem + { + Name = a.Name, + Summary = a.Summary + }) + .ToList(); + + return $@"SCHEMA +{JsonSerializer.Serialize(schemaPayload, _prettyJsonOptions)} + +DATA +{JsonSerializer.Serialize(dataPayload, _prettyJsonOptions)} + +ATTACHMENTS +{JsonSerializer.Serialize(analysisAttachments, _prettyJsonOptions)} + +RUBRIC +{rubricText ?? AnalysisPrompts.DefaultRubric} + +SCORE +{AnalysisPrompts.ScoreRules} + +OUTPUT +{AnalysisPrompts.OutputTemplate} + +RULES +{AnalysisPrompts.Rules}"; + } + // Canonical scoresheet-all prompt contract is defined by: + // ScoresheetPrompts.AllSystemPrompt, ScoresheetPrompts.AllOutputTemplate, and ScoresheetPrompts.AllRules. + public async Task GenerateScoresheetAllAnswersAsync(ScoresheetAllRequest request) + { + if (!HasApiKey) + { + _logger.LogWarning("{Message}", NoApiKeyMessage); + return EmptyJsonObject; + } + + try + { + if (request == null) + { + _logger.LogWarning("Scoresheet-all request was null."); + return EmptyJsonObject; + } + + if (!IsValidScoresheetQuestionsPayload(request.Questions)) + { + _logger.LogWarning( + "Invalid scoresheet-all questions payload shape. Questions kind: {QuestionsKind}.", + request.Questions.ValueKind); + return EmptyJsonObject; + } + + var scoresheetPrompt = BuildScoresheetAllPrompt(request); + return await ExecutePromptWithRetryAsync( + promptType: "ScoresheetAll", + systemPrompt: ScoresheetPrompts.AllSystemPrompt, + userPrompt: scoresheetPrompt, + maxTokens: ScoresheetAllMaxTokens, + normalizeResponse: NormalizeScoresheetAllResponse, + isValidNormalizedResponse: normalized => !IsEmptyJsonObject(normalized), + fallbackResponse: EmptyJsonObject); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error generating scoresheet answers"); + return EmptyJsonObject; + } + } + // Canonical scoresheet-section prompt contract is defined by: + // ScoresheetPrompts.SectionSystemPrompt, ScoresheetPrompts.SectionOutputTemplate, and ScoresheetPrompts.SectionRules. + public async Task GenerateScoresheetSectionAnswersAsync(ScoresheetSectionRequest request) + { + if (!HasApiKey) + { + _logger.LogWarning("{Message}", NoApiKeyMessage); + return EmptyJsonObject; + } + + try + { + if (request == null) + { + _logger.LogWarning("Scoresheet-section request was null."); + return EmptyJsonObject; + } + + if (!IsValidScoresheetSectionSchemaPayload(request.SectionSchema)) + { + _logger.LogWarning( + "Invalid scoresheet-section schema payload shape. SectionSchema kind: {SectionSchemaKind}.", + request.SectionSchema.ValueKind); + return EmptyJsonObject; + } + + var scoresheetSectionPrompt = BuildScoresheetSectionPrompt(request); + return await ExecutePromptWithRetryAsync( + promptType: "ScoresheetSection", + systemPrompt: ScoresheetPrompts.SectionSystemPrompt, + userPrompt: scoresheetSectionPrompt, + maxTokens: ScoresheetSectionMaxTokens, + normalizeResponse: raw => NormalizeScoresheetSectionResponse(raw, request.SectionSchema), + isValidNormalizedResponse: normalized => IsCompleteScoresheetSectionResponse(normalized, request.SectionSchema), + fallbackResponse: EmptyJsonObject); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error generating scoresheet section answers for section {SectionName}", request?.SectionName); + return EmptyJsonObject; + } + } + + private string BuildScoresheetAllPrompt(ScoresheetAllRequest request) + { + var emptyObject = CreateEmptyJsonObject(); + var questionsPayload = request.Questions.ValueKind == JsonValueKind.Undefined ? emptyObject : request.Questions; + var dataPayload = request.Data.ValueKind == JsonValueKind.Undefined ? emptyObject : request.Data; + var attachmentsPayload = BuildScoresheetAttachmentPromptItems(request.Attachments); + + return $@"DATA +{JsonSerializer.Serialize(dataPayload, _prettyJsonOptions)} + +ATTACHMENTS +{JsonSerializer.Serialize(attachmentsPayload, _prettyJsonOptions)} + +QUESTIONS +{JsonSerializer.Serialize(questionsPayload, _prettyJsonOptions)} + +OUTPUT +{ScoresheetPrompts.AllOutputTemplate} + +RULES +{ScoresheetPrompts.AllRules}"; + } + + private string BuildScoresheetSectionPrompt(ScoresheetSectionRequest request) + { + var emptyObject = CreateEmptyJsonObject(); + var sectionSchemaPayload = request.SectionSchema.ValueKind == JsonValueKind.Undefined ? emptyObject : request.SectionSchema; + var dataPayload = request.Data.ValueKind == JsonValueKind.Undefined ? emptyObject : request.Data; + var attachmentsPayload = BuildScoresheetAttachmentPromptItems(request.Attachments); + var responseTemplate = BuildScoresheetSectionResponseTemplate(sectionSchemaPayload); + var sectionName = request.SectionName ?? string.Empty; + var section = new + { + name = sectionName, + questions = sectionSchemaPayload + }; + + return $@"DATA +{JsonSerializer.Serialize(dataPayload, _prettyJsonOptions)} + +ATTACHMENTS +{JsonSerializer.Serialize(attachmentsPayload, _prettyJsonOptions)} + +SECTION +{JsonSerializer.Serialize(section, _prettyJsonOptions)} + +RESPONSE +{JsonSerializer.Serialize(responseTemplate, _prettyJsonOptions)} + +OUTPUT +{ScoresheetPrompts.SectionOutputTemplate} + +RULES +{ScoresheetPrompts.SectionRules}"; + } + + private static Dictionary BuildScoresheetSectionResponseTemplate(JsonElement sectionSchemaPayload) + { + var template = new Dictionary(StringComparer.Ordinal); + var questions = EnumerateSectionQuestions(sectionSchemaPayload); + + foreach (var question in questions) + { + if (!TryGetQuestionId(question, out var questionId)) + { + continue; + } + + template[questionId] = new Dictionary + { + [AIJsonKeys.Answer] = string.Empty, + [AIJsonKeys.Rationale] = string.Empty, + [AIJsonKeys.Confidence] = 0 + }; + } + + return template; + } + + private static List BuildScoresheetAttachmentPromptItems(List attachments) + { + return attachments? + .Where(attachment => attachment != null && !string.IsNullOrWhiteSpace(attachment.Summary)) + .Select(attachment => (object)new + { + name = string.IsNullOrWhiteSpace(attachment.Name) ? "attachment" : attachment.Name.Trim(), + summary = attachment.Summary.Trim() + }) + .ToList() ?? new List(); + } + + private static bool IsValidScoresheetQuestionsPayload(JsonElement questions) + { + return questions.ValueKind == JsonValueKind.Object || questions.ValueKind == JsonValueKind.Array; + } + + private static bool IsValidScoresheetSectionSchemaPayload(JsonElement sectionSchema) + { + return sectionSchema.ValueKind == JsonValueKind.Object || sectionSchema.ValueKind == JsonValueKind.Array; + } + + private string BuildEmptyAnalysisResponseJson() + { + var emptyResponse = new ApplicationAnalysisResponse + { + Rating = ScoreMedium, + Errors = new List(), + Warnings = new List(), + Summaries = new List(), + Dismissed = new List() + }; + + return JsonSerializer.Serialize(emptyResponse, _prettyJsonOptions); + } + + private static JsonElement CreateEmptyJsonObject() + { + return JsonSerializer.SerializeToElement(new { }); + } + + private string NormalizeScoresheetAllResponse(string response) + { + if (!TryParseJsonObjectFromResponse(response, out var responseObject)) + { + _logger.LogError("Invalid scoresheet-all JSON response."); + return EmptyJsonObject; + } + + return JsonSerializer.Serialize(responseObject, _prettyJsonOptions); + } + + private string NormalizeScoresheetSectionResponse(string response, JsonElement sectionSchemaPayload) + { + if (!TryParseJsonObjectFromResponse(response, out var responseObject)) + { + _logger.LogError("Invalid scoresheet-section JSON response."); + return EmptyJsonObject; + } + + var questionSpecs = BuildSectionQuestionSpecs(sectionSchemaPayload); + var normalized = new Dictionary(); + IEnumerable questionIds = questionSpecs.Count > 0 + ? questionSpecs.Keys + : responseObject.EnumerateObject().Select(p => p.Name); + + foreach (var questionId in questionIds) + { + responseObject.TryGetProperty(questionId, out var value); + var answer = value.ValueKind == JsonValueKind.Undefined ? string.Empty : value.ToString(); + var rationale = string.Empty; + var confidence = 0; + + if (value.ValueKind == JsonValueKind.Object) + { + if (value.TryGetProperty(AIJsonKeys.Answer, out var answerProp)) + { + answer = answerProp.ToString(); + } + + if (value.TryGetProperty(AIJsonKeys.Rationale, out var rationaleProp)) + { + rationale = rationaleProp.ToString(); + } + + if (value.TryGetProperty(AIJsonKeys.Confidence, out var confidenceProp)) + { + confidence = NormalizeConfidenceIncrement(ParseConfidenceValue(confidenceProp)); + } + } + + questionSpecs.TryGetValue(questionId, out var questionSpec); + var normalizedAnswer = NormalizeAnswerByQuestionType(answer, questionSpec); + var normalizedRationale = rationale?.Trim() ?? string.Empty; + var normalizedConfidence = NormalizeConfidenceIncrement(confidence); + + normalized[questionId] = new Dictionary + { + [AIJsonKeys.Answer] = normalizedAnswer, + [AIJsonKeys.Rationale] = normalizedRationale, + [AIJsonKeys.Confidence] = normalizedConfidence + }; + } + + return JsonSerializer.Serialize(normalized, _prettyJsonOptions); + } + + private static Dictionary BuildSectionQuestionSpecs(JsonElement sectionSchemaPayload) + { + var specs = new Dictionary(StringComparer.Ordinal); + foreach (var question in EnumerateSectionQuestions(sectionSchemaPayload)) + { + if (!TryGetQuestionId(question, out var questionId)) + { + continue; + } + + var spec = new SectionQuestionSpec + { + QuestionType = question.TryGetProperty("type", out var typeProp) + ? typeProp.GetString() ?? string.Empty + : string.Empty + }; + + if (question.TryGetProperty("options", out var options) && options.ValueKind == JsonValueKind.Array) + { + foreach (var option in options.EnumerateArray()) + { + if (!option.TryGetProperty("number", out var numberProp)) + { + continue; + } + + var number = numberProp.ValueKind == JsonValueKind.Number + ? numberProp.GetInt32().ToString() + : numberProp.ToString(); + + if (string.IsNullOrWhiteSpace(number)) + { + continue; + } + + spec.OptionNumbers.Add(number); + var label = option.TryGetProperty("value", out var valueProp) ? valueProp.ToString() : string.Empty; + spec.OptionLabels[number] = label ?? string.Empty; + } + } + + specs[questionId] = spec; + } + + return specs; + } + + private static IEnumerable EnumerateSectionQuestions(JsonElement sectionSchemaPayload) + { + if (sectionSchemaPayload.ValueKind == JsonValueKind.Array) + { + foreach (var question in sectionSchemaPayload.EnumerateArray()) + { + if (question.ValueKind == JsonValueKind.Object) + { + yield return question; + } + } + } + else if (sectionSchemaPayload.ValueKind == JsonValueKind.Object && + sectionSchemaPayload.TryGetProperty("questions", out var questions) && + questions.ValueKind == JsonValueKind.Array) + { + foreach (var question in questions.EnumerateArray()) + { + if (question.ValueKind == JsonValueKind.Object) + { + yield return question; + } + } + } + } + + private static bool TryGetQuestionId(JsonElement question, out string questionId) + { + questionId = string.Empty; + if (!question.TryGetProperty("id", out var idProp) || idProp.ValueKind != JsonValueKind.String) + { + return false; + } + + questionId = idProp.GetString() ?? string.Empty; + return !string.IsNullOrWhiteSpace(questionId); + } + + private static object NormalizeAnswerByQuestionType(string answer, SectionQuestionSpec? questionSpec) + { + var normalizedAnswer = answer?.Trim() ?? string.Empty; + var questionType = questionSpec?.QuestionType ?? string.Empty; + + if (questionType.Equals("YesNo", StringComparison.OrdinalIgnoreCase)) + { + if (normalizedAnswer.Equals("Yes", StringComparison.OrdinalIgnoreCase)) + { + return "Yes"; + } + + if (normalizedAnswer.Equals("No", StringComparison.OrdinalIgnoreCase)) + { + return "No"; + } + + return "No"; + } + + if (questionType.Equals("Number", StringComparison.OrdinalIgnoreCase)) + { + if (decimal.TryParse(normalizedAnswer, out var decimalAnswer)) + { + return decimalAnswer; + } + + return 0; + } + + if (questionType.Equals("SelectList", StringComparison.OrdinalIgnoreCase)) + { + return NormalizeSelectListAnswer(normalizedAnswer, questionSpec); + } + + if (questionType.Equals("Text", StringComparison.OrdinalIgnoreCase) || + questionType.Equals("TextArea", StringComparison.OrdinalIgnoreCase)) + { + return normalizedAnswer; + } + + return normalizedAnswer; + } + + private static string NormalizeSelectListAnswer(string answer, SectionQuestionSpec? questionSpec) + { + var options = questionSpec?.OptionNumbers ?? new List(); + if (options.Count == 0) + { + return answer; + } + + if (options.Contains(answer)) + { + return answer; + } + + if (int.TryParse(answer, out var parsedAnswer) && options.Contains(parsedAnswer.ToString())) + { + return parsedAnswer.ToString(); + } + + return answer; + } + + private sealed class SectionQuestionSpec + { + public string QuestionType { get; set; } = string.Empty; + public List OptionNumbers { get; set; } = new(); + public Dictionary OptionLabels { get; set; } = new(StringComparer.OrdinalIgnoreCase); + } + + private static int NormalizeConfidenceIncrement(int confidence) + { + var rounded = (int)Math.Round(confidence / 5.0, MidpointRounding.AwayFromZero) * 5; + return Math.Clamp(rounded, 0, 100); + } + + private static int ParseConfidenceValue(JsonElement confidenceProp) + { + if (confidenceProp.ValueKind == JsonValueKind.Number) + { + if (confidenceProp.TryGetInt32(out var intValue)) + { + return intValue; + } + + if (confidenceProp.TryGetDouble(out var doubleValue)) + { + return (int)Math.Round(doubleValue, MidpointRounding.AwayFromZero); + } + } + + if (confidenceProp.ValueKind == JsonValueKind.String) + { + var raw = confidenceProp.GetString(); + if (int.TryParse(raw, out var parsedInt)) + { + return parsedInt; + } + + if (double.TryParse(raw, out var parsedDouble)) + { + return (int)Math.Round(parsedDouble, MidpointRounding.AwayFromZero); + } + } + + return 0; + } + + private void LogPromptOutput(string promptType, string output) + { + if (!LogPayloads) + { + return; + } + + var formattedOutput = FormatPromptOutputForLog(promptType, output); + _logger.LogDebug( + "AI {PromptType} model output payload: {ModelOutput}", + promptType, + formattedOutput); + WriteAiPromptLog(promptType, "OUTPUT", formattedOutput); + } + + private void LogPromptInput(string promptType, string? systemPrompt, string userPrompt) + { + if (!LogPayloads) + { + return; + } + + var formattedInput = FormatPromptInputForLog(systemPrompt, userPrompt); + _logger.LogDebug( + "AI {PromptType} input payload: {PromptInput}", + promptType, + formattedInput); + WriteAiPromptLog(promptType, "INPUT", formattedInput); + } + + private void WriteAiPromptLog(string promptType, string payloadType, string payload) + { + if (!LogPayloads) + { + return; + } + + try + { + var now = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss zzz"); + var logPath = Path.Combine(AppContext.BaseDirectory, AiPromptLogRelativePath); + EnsureAiPromptLogInitialized(logPath); + + var entry = $"{now} [{promptType}] {payloadType}\n{payload}\n\n"; + File.AppendAllText(logPath, entry); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to write AI prompt log file."); + } + } + + private static void EnsureAiPromptLogInitialized(string logPath) + { + var directory = Path.GetDirectoryName(logPath); + if (!string.IsNullOrWhiteSpace(directory)) + { + Directory.CreateDirectory(directory); + } + + // Reset once per process run so each fresh app run starts with a clean AI prompt log. + if (Interlocked.Exchange(ref _aiPromptLogInitialized, 1) == 0) + { + File.WriteAllText(logPath, string.Empty); + } + } + + private static string FormatPromptInputForLog(string? systemPrompt, string userPrompt) + { + var normalizedSystemPrompt = string.IsNullOrWhiteSpace(systemPrompt) ? string.Empty : systemPrompt.Trim(); + var normalizedUserPrompt = string.IsNullOrWhiteSpace(userPrompt) ? string.Empty : userPrompt.Trim(); + return $"SYSTEM_PROMPT\n{normalizedSystemPrompt}\n\nUSER_PROMPT\n{normalizedUserPrompt}"; + } + + private string FormatPromptOutputForLog(string promptType, string output) + { + if (string.IsNullOrWhiteSpace(output)) + { + return string.Empty; + } + + // For JSON contracts, log only normalized payload JSON. + if (TryParseJsonObjectFromResponse(output, out var jsonObject)) + { + return JsonSerializer.Serialize(jsonObject, _prettyJsonOptions); + } + + return output.Trim(); + } + + private static bool TryParseJsonObjectFromResponse(string response, out JsonElement objectElement) + { + objectElement = default; + var cleaned = CleanJsonResponse(response); + if (string.IsNullOrWhiteSpace(cleaned)) + { + return false; + } + + try + { + using var doc = JsonDocument.Parse(cleaned); + if (doc.RootElement.ValueKind != JsonValueKind.Object) + { + return false; + } + + objectElement = doc.RootElement.Clone(); + return true; + } + catch (JsonException) + { + return false; + } + } + + private static bool IsEmptyJsonObject(string json) + { + if (string.IsNullOrWhiteSpace(json)) + { + return true; + } + + try + { + using var doc = JsonDocument.Parse(json); + return doc.RootElement.ValueKind == JsonValueKind.Object && + !doc.RootElement.EnumerateObject().Any(); + } + catch (JsonException) + { + return true; + } + } + + private async Task ExecutePromptWithRetryAsync( + string promptType, + string systemPrompt, + string userPrompt, + int maxTokens, + Func normalizeResponse, + Func isValidNormalizedResponse, + string fallbackResponse, + int maxAttempts = 2) + { + LogPromptInput(promptType, systemPrompt, userPrompt); + + for (var attempt = 1; attempt <= maxAttempts; attempt++) + { + var rawResponse = await GenerateCompletionAsync(new AICompletionRequest + { + UserPrompt = userPrompt, + SystemPrompt = systemPrompt, + MaxTokens = maxTokens + }); + + var outputType = attempt == 1 ? promptType : $"{promptType}Retry"; + LogPromptOutput(outputType, rawResponse); + + var normalized = normalizeResponse(rawResponse); + if (isValidNormalizedResponse(normalized)) + { + return normalized; + } + + if (attempt < maxAttempts) + { + _logger.LogWarning( + "{PromptType} response failed output-shape validation on attempt {Attempt}/{MaxAttempts}. Retrying.", + promptType, + attempt, + maxAttempts); + } + } + + return fallbackResponse; + } + + private static bool IsValidAnalysisNormalizedResponse(string normalizedJson) + { + if (!TryParseJsonObjectFromResponse(normalizedJson, out var root)) + { + return false; + } + + return root.TryGetProperty("rating", out _) && + root.TryGetProperty("errors", out _) && + root.TryGetProperty("warnings", out _) && + root.TryGetProperty("summaries", out _); + } + + private static bool IsCompleteScoresheetSectionResponse(string normalizedJson, JsonElement sectionSchemaPayload) + { + if (!TryParseJsonObjectFromResponse(normalizedJson, out var root)) + { + return false; + } + + var expectedQuestionIds = EnumerateSectionQuestions(sectionSchemaPayload) + .Select(q => TryGetQuestionId(q, out var id) ? id : string.Empty) + .Where(id => !string.IsNullOrWhiteSpace(id)) + .ToList(); + + if (expectedQuestionIds.Count == 0) + { + return false; + } + + foreach (var questionId in expectedQuestionIds) + { + if (!root.TryGetProperty(questionId, out var answerObject) || answerObject.ValueKind != JsonValueKind.Object) + { + return false; + } + + if (!answerObject.TryGetProperty(AIJsonKeys.Answer, out var answerProp) || + string.IsNullOrWhiteSpace(answerProp.ToString())) + { + return false; + } + + if (!answerObject.TryGetProperty(AIJsonKeys.Rationale, out var rationaleProp) || + string.IsNullOrWhiteSpace(rationaleProp.ToString())) + { + return false; + } + + if (!answerObject.TryGetProperty(AIJsonKeys.Confidence, out _)) + { + return false; + } + } + + return true; + } + + private static string CleanJsonResponse(string response) + { + if (string.IsNullOrWhiteSpace(response)) + { + return string.Empty; + } + + var cleaned = response.Trim(); + + if (cleaned.StartsWith("```json", StringComparison.OrdinalIgnoreCase) || cleaned.StartsWith("```")) + { + var startIndex = cleaned.IndexOf('\n'); + if (startIndex >= 0) + { + cleaned = cleaned.Substring(startIndex + 1); + } + } + + if (cleaned.EndsWith("```", StringComparison.Ordinal)) + { + var lastIndex = cleaned.LastIndexOf("```", StringComparison.Ordinal); + if (lastIndex > 0) + { + cleaned = cleaned.Substring(0, lastIndex); + } + } + + return cleaned.Trim(); + } + } +} + + diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/PromptCoreRules.v1.txt b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/PromptCoreRules.v1.txt new file mode 100644 index 0000000000..e11dce3c97 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/PromptCoreRules.v1.txt @@ -0,0 +1,13 @@ +namespace Unity.GrantManager.AI +{ + internal static class PromptCoreRules + { + public const string UseProvidedEvidence = "- Use only provided input sections as evidence."; + public const string NoInvention = "- Do not invent missing details."; + public const string MinimumNarrativeWords = "- Any narrative text response must be at least 12 words."; + public const string ExactOutputShape = "- Return values exactly as specified in OUTPUT."; + public const string NoExtraOutputKeys = "- Do not return keys outside OUTPUT."; + public const string ValidJsonOnly = "- Return valid JSON only."; + public const string PlainJsonOnly = "- Return plain JSON only (no markdown)."; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/PromptHeader.v1.txt b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/PromptHeader.v1.txt new file mode 100644 index 0000000000..701a43e740 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/PromptHeader.v1.txt @@ -0,0 +1,14 @@ +namespace Unity.GrantManager.AI +{ + internal static class PromptHeader + { + public static string Build(string role, string task) + { + return $@"ROLE +{role} + +TASK +{task}"; + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/ScoresheetPrompts.v1.txt b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/ScoresheetPrompts.v1.txt new file mode 100644 index 0000000000..bfe883d643 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/ScoresheetPrompts.v1.txt @@ -0,0 +1,85 @@ +namespace Unity.GrantManager.AI +{ + internal static class ScoresheetPrompts + { + public static readonly string AllSystemPrompt = PromptHeader.Build( + "You are an expert grant application reviewer for the BC Government.", + "Using DATA, ATTACHMENTS, QUESTIONS, OUTPUT, and RULES, provide answers for all scoresheet questions."); + + public const string AllOutputTemplate = @"{ + """": """" +}"; + + public const string AllRules = "- Use only DATA and ATTACHMENTS as evidence.\n" + + "- Do not invent missing application details.\n" + + @"- Return exactly one answer per question ID in QUESTIONS. +- Do not omit any question IDs from QUESTIONS. +- Do not add keys that are not question IDs from QUESTIONS. +- The ""answer"" value type must match the question type. +- For numeric questions, return a numeric value within the allowed range. +- For yes/no questions, return exactly ""Yes"" or ""No"". +- For select list questions, return only the selected options.number as a string and never return option label text. +- For text and text area questions, return concise, evidence-based text. +- For text and text area questions, include concise source-grounded rationale from the provided input content. +- If explicit evidence is insufficient, choose the most conservative valid answer. +" + + PromptCoreRules.MinimumNarrativeWords + "\n" + + PromptCoreRules.ExactOutputShape + "\n" + + PromptCoreRules.NoExtraOutputKeys + "\n" + + PromptCoreRules.ValidJsonOnly + "\n" + + PromptCoreRules.PlainJsonOnly; + + public static readonly string SectionSystemPrompt = PromptHeader.Build( + "You are an expert grant application reviewer for the BC Government.", + "Using DATA, ATTACHMENTS, SECTION, RESPONSE, OUTPUT, and RULES, answer only the questions in SECTION."); + + public const string SectionOutputTemplate = @"{ + """": { + ""answer"": """", + ""rationale"": """", + ""confidence"": + } +}"; + + public const string SectionRules = "- Use only DATA and ATTACHMENTS as evidence.\n" + + "- Do not invent missing application details.\n" + + @"- Return exactly one answer object per question ID in SECTION.questions. +- Do not omit any question IDs from SECTION.questions. +- Do not add keys that are not question IDs from SECTION.questions. +- Use RESPONSE as the output contract and fill every placeholder value. +- Follow this process in order: (1) copy RESPONSE, (2) iterate SECTION.questions in order, (3) fill answer+rationale+confidence for each matching question ID, (4) run final completeness check. +- Each answer object must include: ""answer"", ""rationale"", and ""confidence"". +- Never omit ""answer"", ""rationale"", or ""confidence"" for any question type. +- The ""answer"" value type must match question type: Number => numeric; YesNo/SelectList/Text/TextArea => string. +- The ""rationale"" field must be 1-2 complete sentences and grounded in concrete DATA/ATTACHMENTS evidence. +- In ""rationale"", cite concrete source evidence from the provided input content; do not cite prompt section headers. +- For every question, rationale must justify both the selected answer and the selected confidence level based on evidence strength. +- If explicit evidence is insufficient, choose the most conservative valid answer and state uncertainty in rationale. +- Do not treat missing or non-contradictory information as evidence. +- The ""confidence"" field must be an integer from 0 to 100 in increments of 5 and represents confidence in the selected answer. +- Set confidence by certainty of the selected answer based on available evidence, regardless of which option is selected. +- For yes/no questions, the ""answer"" field must be exactly ""Yes"" or ""No"". +- For numeric questions, answer must be a numeric value within the allowed range. +- For numeric questions, answer must never be blank. +- If evidence is insufficient for a numeric question, return the minimum allowed numeric value and explain uncertainty in rationale. +- If a required value is explicitly missing in DATA/ATTACHMENTS, set confidence high (80-100) when selecting the conservative minimum. +- For select list questions, return only the selected options.number as a string (the option index shown in options), never label text or points. +- For select list questions, the ""answer"" value must be one of question.allowed_answers exactly. +- Never return 0 for select list answers unless 0 exists as an explicit option number. +- For text and text area questions, answer must be concise, evidence-based, non-empty, and avoid boilerplate placeholders. +- For text and text area questions, answer is the reviewer comment, and rationale must explain the evidence basis and certainty for that comment. +- For comment fields, summarize key evidence-based conclusions from the other questions in SECTION, including uncertainty where applicable. +- Do not leave rationale empty when answer is populated. +- Final self-check before responding: every question ID in RESPONSE must have a non-empty ""answer"", non-empty ""rationale"", and ""confidence"". +- If any answer object is incomplete, regenerate the full JSON response before returning it. +" + + PromptCoreRules.MinimumNarrativeWords + "\n" + + PromptCoreRules.ExactOutputShape + "\n" + + PromptCoreRules.NoExtraOutputKeys + "\n" + + PromptCoreRules.ValidJsonOnly + "\n" + + PromptCoreRules.PlainJsonOnly; + } +} + + + From 9ee1136a83d352d957f8c318bdde34baf2268a22 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 6 Mar 2026 16:21:16 -0800 Subject: [PATCH 08/58] AB#32009 Normalize prompt baseline version folders and snapshots --- .../AI/Prompts/Baselines/README.md | 8 + ...ysisPrompts.v1.txt => AnalysisPrompts.txt} | 0 ...ntPrompts.v1.txt => AttachmentPrompts.txt} | 0 ...Service.v0.cs.txt => OpenAIService.cs.txt} | 0 .../Baselines/v0/OpenAIService.v1.cs.txt | 1134 ----------------- ...ptCoreRules.v1.txt => PromptCoreRules.txt} | 0 .../{PromptHeader.v1.txt => PromptHeader.txt} | 0 ...etPrompts.v1.txt => ScoresheetPrompts.txt} | 0 .../Baselines/v1/AnalysisPrompts.cs.txt | 126 ++ .../Baselines/v1/AttachmentPrompts.cs.txt | 27 + .../Prompts/Baselines/v1/OpenAIService.cs.txt | 833 ++++++++++++ .../Baselines/v1/PromptCoreRules.cs.txt | 13 + .../Prompts/Baselines/v1/PromptHeader.cs.txt | 14 + .../Baselines/v1/ScoresheetPrompts.cs.txt | 80 ++ 14 files changed, 1101 insertions(+), 1134 deletions(-) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/README.md rename applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/{AnalysisPrompts.v1.txt => AnalysisPrompts.txt} (100%) rename applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/{AttachmentPrompts.v1.txt => AttachmentPrompts.txt} (100%) rename applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/{OpenAIService.v0.cs.txt => OpenAIService.cs.txt} (100%) delete mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/OpenAIService.v1.cs.txt rename applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/{PromptCoreRules.v1.txt => PromptCoreRules.txt} (100%) rename applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/{PromptHeader.v1.txt => PromptHeader.txt} (100%) rename applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/{ScoresheetPrompts.v1.txt => ScoresheetPrompts.txt} (100%) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v1/AnalysisPrompts.cs.txt create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v1/AttachmentPrompts.cs.txt create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v1/OpenAIService.cs.txt create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v1/PromptCoreRules.cs.txt create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v1/PromptHeader.cs.txt create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v1/ScoresheetPrompts.cs.txt diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/README.md b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/README.md new file mode 100644 index 0000000000..4106b7aa0c --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/README.md @@ -0,0 +1,8 @@ +# Prompt Baselines + +- `v0`: legacy prompt/service snapshots used as historical baseline. +- `v1`: current runtime prompt/service snapshots. + +Versioning convention: +- Folder name is the baseline version. +- Filenames inside each folder are normalized and do not include version suffixes. diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/AnalysisPrompts.v1.txt b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/AnalysisPrompts.txt similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/AnalysisPrompts.v1.txt rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/AnalysisPrompts.txt diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/AttachmentPrompts.v1.txt b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/AttachmentPrompts.txt similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/AttachmentPrompts.v1.txt rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/AttachmentPrompts.txt diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/OpenAIService.v0.cs.txt b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/OpenAIService.cs.txt similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/OpenAIService.v0.cs.txt rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/OpenAIService.cs.txt diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/OpenAIService.v1.cs.txt b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/OpenAIService.v1.cs.txt deleted file mode 100644 index e421036ebd..0000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/OpenAIService.v1.cs.txt +++ /dev/null @@ -1,1134 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading; -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; - -namespace Unity.GrantManager.AI -{ - public class OpenAIService : IAIService, ITransientDependency - { - private readonly HttpClient _httpClient; - private readonly IConfiguration _configuration; - private readonly ILogger _logger; - private readonly ITextExtractionService _textExtractionService; - private readonly JsonSerializerOptions _prettyJsonOptions = new() { WriteIndented = true }; - - private string? ApiKey => _configuration["AI:OpenAI:ApiKey"]; - private bool HasApiKey => !string.IsNullOrWhiteSpace(ApiKey); - private string? ApiUrl => _configuration["AI:OpenAI:ApiUrl"] ?? "https://api.openai.com/v1/chat/completions"; - private bool LogPayloads => _configuration.GetValue("AI:Logging:LogPayloads") ?? false; - private readonly string NoApiKeyMessage = "OpenAI API key is not configured"; - private const string ServiceNotConfiguredMessage = "AI analysis not available - service not configured."; - private const string ServiceTemporarilyUnavailableMessage = "AI analysis failed - service temporarily unavailable."; - private const string GenericFailureMessage = "AI analysis failed - please try again later."; - private const string NoSummaryGeneratedMessage = "No summary generated."; - private const string EmptyJsonObject = "{}"; - private const int ScoresheetAllMaxTokens = 2000; - private const int ScoresheetSectionMaxTokens = 3200; - private const int AnalysisMaxTokens = 1600; - private const string DefaultContentType = "application/octet-stream"; - private const int AttachmentSummaryMaxTokens = 240; - private const string AttachmentSummaryUnavailableMessage = "AI analysis not available for this attachment"; - private const string ScoreHigh = "HIGH"; - private const string ScoreMedium = "MEDIUM"; - private const string ScoreLow = "LOW"; - private const string AiPromptLogRelativePath = "logs/ai-prompts.log"; - private static int _aiPromptLogInitialized; - - public OpenAIService(HttpClient httpClient, IConfiguration configuration, ILogger logger, ITextExtractionService textExtractionService) - { - _httpClient = httpClient; - _configuration = configuration; - _logger = logger; - _textExtractionService = textExtractionService; - } - - public Task IsAvailableAsync() - { - if (!HasApiKey) - { - _logger.LogWarning("Error: {Message}", NoApiKeyMessage); - return Task.FromResult(false); - } - - return Task.FromResult(true); - } - - public async Task GenerateCompletionAsync(AICompletionRequest request) - { - if (request == null) - { - _logger.LogWarning("AI completion request was null."); - return GenericFailureMessage; - } - - var userPrompt = request.UserPrompt ?? string.Empty; - var systemPrompt = request.SystemPrompt; - var maxTokens = request.MaxTokens <= 0 ? 150 : request.MaxTokens; - var temperature = request.Temperature ?? 0.3; - - return await ExecuteChatCompletionAsync(userPrompt, systemPrompt, maxTokens, temperature); - } - - private async Task ExecuteChatCompletionAsync( - string userPrompt, - string? systemPrompt = null, - int maxTokens = 150, - double temperature = 0.3) - { - if (!HasApiKey) - { - _logger.LogWarning("Error: {Message}", NoApiKeyMessage); - return ServiceNotConfiguredMessage; - } - - _logger.LogDebug( - "Calling OpenAI chat completions. PromptLength: {PromptLength}, MaxTokens: {MaxTokens}", - userPrompt?.Length ?? 0, - maxTokens); - - try - { - string resolvedSystemPrompt = systemPrompt ?? "You are a professional grant analyst for the BC Government."; - - var requestBody = new Dictionary - { - ["messages"] = new[] - { - new { role = "system", content = resolvedSystemPrompt }, - new { role = "user", content = userPrompt ?? string.Empty } - }, - ["max_tokens"] = maxTokens, - ["temperature"] = temperature - }; - - var json = JsonSerializer.Serialize(requestBody); - var authValue = ApiKey!.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase) - ? ApiKey.Substring("Bearer ".Length).Trim() - : ApiKey; - - using var request = new HttpRequestMessage(HttpMethod.Post, ApiUrl); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authValue); - request.Content = new StringContent(json, Encoding.UTF8, "application/json"); - - var response = await _httpClient.SendAsync(request); - var responseContent = await response.Content.ReadAsStringAsync() ?? string.Empty; - - _logger.LogDebug( - "OpenAI chat completions response received. StatusCode: {StatusCode}, ResponseLength: {ResponseLength}", - response.StatusCode, - responseContent?.Length ?? 0); - - if (!response.IsSuccessStatusCode) - { - _logger.LogError("OpenAI API request failed: {StatusCode} - {Content}", response.StatusCode, responseContent); - return ServiceTemporarilyUnavailableMessage; - } - - using var jsonDoc = JsonDocument.Parse(responseContent ?? string.Empty); - var choices = jsonDoc.RootElement.GetProperty("choices"); - if (choices.GetArrayLength() > 0) - { - var message = choices[0].GetProperty("message"); - return message.GetProperty("content").GetString() ?? NoSummaryGeneratedMessage; - } - - return NoSummaryGeneratedMessage; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error generating AI summary"); - return GenericFailureMessage; - } - } - - // Canonical attachment summary prompt contract is defined by: - // AttachmentPrompts.SystemPrompt, AttachmentPrompts.OutputSection, and AttachmentPrompts.RulesSection. - public async Task GenerateAttachmentSummaryAsync(AttachmentSummaryRequest request) - { - try - { - if (request == null) - { - _logger.LogWarning("Attachment summary request was null."); - return $"{AttachmentSummaryUnavailableMessage} (unknown)."; - } - - var normalizedFileName = string.IsNullOrWhiteSpace(request.FileName) ? "unknown" : request.FileName.Trim(); - var normalizedContentType = string.IsNullOrWhiteSpace(request.ContentType) ? DefaultContentType : request.ContentType.Trim(); - var normalizedFileContent = request.FileContent ?? Array.Empty(); - var extractedText = await _textExtractionService.ExtractTextAsync(normalizedFileName, normalizedFileContent, normalizedContentType); - - var hasExtractedText = !string.IsNullOrWhiteSpace(extractedText); - if (hasExtractedText) - { - _logger.LogDebug("Extracted {TextLength} characters from {FileName}", extractedText!.Length, normalizedFileName); - } - else - { - _logger.LogDebug("No text extracted from {FileName}, analyzing metadata only", normalizedFileName); - } - - var attachmentInput = new AttachmentPromptInput - { - Name = normalizedFileName, - Text = hasExtractedText ? extractedText : null - }; - - var contentToAnalyze = BuildAttachmentSummaryPrompt(attachmentInput); - return await ExecutePromptWithRetryAsync( - promptType: "AttachmentSummary", - systemPrompt: AttachmentPrompts.SystemPrompt, - userPrompt: contentToAnalyze, - maxTokens: AttachmentSummaryMaxTokens, - normalizeResponse: NormalizeAttachmentSummaryResponse, - isValidNormalizedResponse: normalized => !string.IsNullOrWhiteSpace(normalized), - fallbackResponse: string.Empty); - } - catch (Exception ex) - { - var fileName = request?.FileName ?? "unknown"; - _logger.LogError(ex, "Error generating attachment summary for {FileName}", fileName); - return $"{AttachmentSummaryUnavailableMessage} ({fileName})."; - } - } - - private string BuildAttachmentSummaryPrompt(AttachmentPromptInput attachmentInput) - { - return $@"ATTACHMENT -{JsonSerializer.Serialize(attachmentInput, _prettyJsonOptions)} - -{AttachmentPrompts.OutputSection} - -RULES -{AttachmentPrompts.RulesSection}"; - } - - private string NormalizeAttachmentSummaryResponse(string response) - { - if (!TryParseJsonObjectFromResponse(response, out var responseObject)) - { - return string.Empty; - } - - if (responseObject.TryGetProperty(AIJsonKeys.Summary, out var summaryProp) && - summaryProp.ValueKind == JsonValueKind.String) - { - return summaryProp.GetString()?.Trim() ?? string.Empty; - } - - return responseObject.ToString().Trim(); - } - - // Canonical analysis prompt contract is defined by: - // AnalysisPrompts.DefaultRubric, AnalysisPrompts.ScoreRules, AnalysisPrompts.OutputTemplate, - // AnalysisPrompts.Rules, and AnalysisPrompts.SystemPrompt. - public async Task GenerateApplicationAnalysisAsync(ApplicationAnalysisRequest request) - { - if (!HasApiKey) - { - _logger.LogWarning("{Message}", NoApiKeyMessage); - return ServiceNotConfiguredMessage; - } - - try - { - if (request == null) - { - _logger.LogWarning("Application analysis request was null."); - return BuildEmptyAnalysisResponseJson(); - } - - var emptyObject = CreateEmptyJsonObject(); - var schemaPayload = request.Schema.ValueKind == JsonValueKind.Undefined ? emptyObject : request.Schema; - var dataPayload = request.Data.ValueKind == JsonValueKind.Undefined ? emptyObject : request.Data; - - if (schemaPayload.ValueKind != JsonValueKind.Object || dataPayload.ValueKind != JsonValueKind.Object) - { - _logger.LogWarning( - "Invalid application analysis request payload shape. Schema kind: {SchemaKind}, Data kind: {DataKind}.", - schemaPayload.ValueKind, - dataPayload.ValueKind); - return BuildEmptyAnalysisResponseJson(); - } - - var attachmentsPayload = request.Attachments? - .Where(a => a != null && !string.IsNullOrWhiteSpace(a.Summary)) - .Select(a => new ApplicationAnalysisAttachment - { - Name = string.IsNullOrWhiteSpace(a.Name) ? "attachment" : a.Name.Trim(), - Summary = a.Summary.Trim() - }) - .ToList() ?? new List(); - - var rubricText = !string.IsNullOrWhiteSpace(request.Rubric) ? request.Rubric : AnalysisPrompts.DefaultRubric; - var analysisPrompt = BuildAnalysisPrompt(schemaPayload, dataPayload, attachmentsPayload, rubricText); - return await ExecutePromptWithRetryAsync( - promptType: "ApplicationAnalysis", - systemPrompt: AnalysisPrompts.SystemPrompt, - userPrompt: analysisPrompt, - maxTokens: AnalysisMaxTokens, - normalizeResponse: NormalizeAnalysisResponse, - isValidNormalizedResponse: IsValidAnalysisNormalizedResponse, - fallbackResponse: BuildEmptyAnalysisResponseJson()); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error analyzing application"); - return BuildEmptyAnalysisResponseJson(); - } - } - - private string NormalizeAnalysisResponse(string analysisJson) - { - try - { - if (!TryParseJsonObjectFromResponse(analysisJson, out var analysisObject)) - { - _logger.LogError("Invalid analysis JSON response."); - return BuildEmptyAnalysisResponseJson(); - } - - var parseOptions = new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true - }; - - var model = JsonSerializer.Deserialize(analysisObject.GetRawText(), parseOptions); - if (model == null) - { - return BuildEmptyAnalysisResponseJson(); - } - - model.Errors ??= new List(); - model.Warnings ??= new List(); - model.Summaries ??= new List(); - model.Dismissed ??= new List(); - - model.Rating = NormalizeRating(model.Rating); - - foreach (var error in model.Errors) - { - error.Id = string.IsNullOrWhiteSpace(error.Id) ? Guid.NewGuid().ToString() : error.Id; - } - - foreach (var warning in model.Warnings) - { - warning.Id = string.IsNullOrWhiteSpace(warning.Id) ? Guid.NewGuid().ToString() : warning.Id; - } - - model.Dismissed = model.Dismissed - .Where(id => !string.IsNullOrWhiteSpace(id)) - .Distinct(StringComparer.Ordinal) - .ToList(); - - var normalizedOutput = new ApplicationAnalysisResponse - { - Rating = model.Rating, - Errors = model.Errors, - Warnings = model.Warnings, - Summaries = model.Summaries, - Dismissed = model.Dismissed - }; - - return JsonSerializer.Serialize(normalizedOutput, _prettyJsonOptions); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error normalizing analysis response."); - return BuildEmptyAnalysisResponseJson(); - } - } - - private static string NormalizeRating(string? rating) - { - var normalized = rating?.Trim().ToUpperInvariant(); - return normalized switch - { - ScoreHigh => ScoreHigh, - ScoreMedium => ScoreMedium, - ScoreLow => ScoreLow, - _ => ScoreMedium - }; - } - - private string BuildAnalysisPrompt( - JsonElement schemaPayload, - JsonElement dataPayload, - List attachmentsPayload, - string rubricText) - { - var analysisAttachments = (attachmentsPayload ?? new List()) - .Select(a => new AnalysisAttachmentPromptItem - { - Name = a.Name, - Summary = a.Summary - }) - .ToList(); - - return $@"SCHEMA -{JsonSerializer.Serialize(schemaPayload, _prettyJsonOptions)} - -DATA -{JsonSerializer.Serialize(dataPayload, _prettyJsonOptions)} - -ATTACHMENTS -{JsonSerializer.Serialize(analysisAttachments, _prettyJsonOptions)} - -RUBRIC -{rubricText ?? AnalysisPrompts.DefaultRubric} - -SCORE -{AnalysisPrompts.ScoreRules} - -OUTPUT -{AnalysisPrompts.OutputTemplate} - -RULES -{AnalysisPrompts.Rules}"; - } - // Canonical scoresheet-all prompt contract is defined by: - // ScoresheetPrompts.AllSystemPrompt, ScoresheetPrompts.AllOutputTemplate, and ScoresheetPrompts.AllRules. - public async Task GenerateScoresheetAllAnswersAsync(ScoresheetAllRequest request) - { - if (!HasApiKey) - { - _logger.LogWarning("{Message}", NoApiKeyMessage); - return EmptyJsonObject; - } - - try - { - if (request == null) - { - _logger.LogWarning("Scoresheet-all request was null."); - return EmptyJsonObject; - } - - if (!IsValidScoresheetQuestionsPayload(request.Questions)) - { - _logger.LogWarning( - "Invalid scoresheet-all questions payload shape. Questions kind: {QuestionsKind}.", - request.Questions.ValueKind); - return EmptyJsonObject; - } - - var scoresheetPrompt = BuildScoresheetAllPrompt(request); - return await ExecutePromptWithRetryAsync( - promptType: "ScoresheetAll", - systemPrompt: ScoresheetPrompts.AllSystemPrompt, - userPrompt: scoresheetPrompt, - maxTokens: ScoresheetAllMaxTokens, - normalizeResponse: NormalizeScoresheetAllResponse, - isValidNormalizedResponse: normalized => !IsEmptyJsonObject(normalized), - fallbackResponse: EmptyJsonObject); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error generating scoresheet answers"); - return EmptyJsonObject; - } - } - // Canonical scoresheet-section prompt contract is defined by: - // ScoresheetPrompts.SectionSystemPrompt, ScoresheetPrompts.SectionOutputTemplate, and ScoresheetPrompts.SectionRules. - public async Task GenerateScoresheetSectionAnswersAsync(ScoresheetSectionRequest request) - { - if (!HasApiKey) - { - _logger.LogWarning("{Message}", NoApiKeyMessage); - return EmptyJsonObject; - } - - try - { - if (request == null) - { - _logger.LogWarning("Scoresheet-section request was null."); - return EmptyJsonObject; - } - - if (!IsValidScoresheetSectionSchemaPayload(request.SectionSchema)) - { - _logger.LogWarning( - "Invalid scoresheet-section schema payload shape. SectionSchema kind: {SectionSchemaKind}.", - request.SectionSchema.ValueKind); - return EmptyJsonObject; - } - - var scoresheetSectionPrompt = BuildScoresheetSectionPrompt(request); - return await ExecutePromptWithRetryAsync( - promptType: "ScoresheetSection", - systemPrompt: ScoresheetPrompts.SectionSystemPrompt, - userPrompt: scoresheetSectionPrompt, - maxTokens: ScoresheetSectionMaxTokens, - normalizeResponse: raw => NormalizeScoresheetSectionResponse(raw, request.SectionSchema), - isValidNormalizedResponse: normalized => IsCompleteScoresheetSectionResponse(normalized, request.SectionSchema), - fallbackResponse: EmptyJsonObject); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error generating scoresheet section answers for section {SectionName}", request?.SectionName); - return EmptyJsonObject; - } - } - - private string BuildScoresheetAllPrompt(ScoresheetAllRequest request) - { - var emptyObject = CreateEmptyJsonObject(); - var questionsPayload = request.Questions.ValueKind == JsonValueKind.Undefined ? emptyObject : request.Questions; - var dataPayload = request.Data.ValueKind == JsonValueKind.Undefined ? emptyObject : request.Data; - var attachmentsPayload = BuildScoresheetAttachmentPromptItems(request.Attachments); - - return $@"DATA -{JsonSerializer.Serialize(dataPayload, _prettyJsonOptions)} - -ATTACHMENTS -{JsonSerializer.Serialize(attachmentsPayload, _prettyJsonOptions)} - -QUESTIONS -{JsonSerializer.Serialize(questionsPayload, _prettyJsonOptions)} - -OUTPUT -{ScoresheetPrompts.AllOutputTemplate} - -RULES -{ScoresheetPrompts.AllRules}"; - } - - private string BuildScoresheetSectionPrompt(ScoresheetSectionRequest request) - { - var emptyObject = CreateEmptyJsonObject(); - var sectionSchemaPayload = request.SectionSchema.ValueKind == JsonValueKind.Undefined ? emptyObject : request.SectionSchema; - var dataPayload = request.Data.ValueKind == JsonValueKind.Undefined ? emptyObject : request.Data; - var attachmentsPayload = BuildScoresheetAttachmentPromptItems(request.Attachments); - var responseTemplate = BuildScoresheetSectionResponseTemplate(sectionSchemaPayload); - var sectionName = request.SectionName ?? string.Empty; - var section = new - { - name = sectionName, - questions = sectionSchemaPayload - }; - - return $@"DATA -{JsonSerializer.Serialize(dataPayload, _prettyJsonOptions)} - -ATTACHMENTS -{JsonSerializer.Serialize(attachmentsPayload, _prettyJsonOptions)} - -SECTION -{JsonSerializer.Serialize(section, _prettyJsonOptions)} - -RESPONSE -{JsonSerializer.Serialize(responseTemplate, _prettyJsonOptions)} - -OUTPUT -{ScoresheetPrompts.SectionOutputTemplate} - -RULES -{ScoresheetPrompts.SectionRules}"; - } - - private static Dictionary BuildScoresheetSectionResponseTemplate(JsonElement sectionSchemaPayload) - { - var template = new Dictionary(StringComparer.Ordinal); - var questions = EnumerateSectionQuestions(sectionSchemaPayload); - - foreach (var question in questions) - { - if (!TryGetQuestionId(question, out var questionId)) - { - continue; - } - - template[questionId] = new Dictionary - { - [AIJsonKeys.Answer] = string.Empty, - [AIJsonKeys.Rationale] = string.Empty, - [AIJsonKeys.Confidence] = 0 - }; - } - - return template; - } - - private static List BuildScoresheetAttachmentPromptItems(List attachments) - { - return attachments? - .Where(attachment => attachment != null && !string.IsNullOrWhiteSpace(attachment.Summary)) - .Select(attachment => (object)new - { - name = string.IsNullOrWhiteSpace(attachment.Name) ? "attachment" : attachment.Name.Trim(), - summary = attachment.Summary.Trim() - }) - .ToList() ?? new List(); - } - - private static bool IsValidScoresheetQuestionsPayload(JsonElement questions) - { - return questions.ValueKind == JsonValueKind.Object || questions.ValueKind == JsonValueKind.Array; - } - - private static bool IsValidScoresheetSectionSchemaPayload(JsonElement sectionSchema) - { - return sectionSchema.ValueKind == JsonValueKind.Object || sectionSchema.ValueKind == JsonValueKind.Array; - } - - private string BuildEmptyAnalysisResponseJson() - { - var emptyResponse = new ApplicationAnalysisResponse - { - Rating = ScoreMedium, - Errors = new List(), - Warnings = new List(), - Summaries = new List(), - Dismissed = new List() - }; - - return JsonSerializer.Serialize(emptyResponse, _prettyJsonOptions); - } - - private static JsonElement CreateEmptyJsonObject() - { - return JsonSerializer.SerializeToElement(new { }); - } - - private string NormalizeScoresheetAllResponse(string response) - { - if (!TryParseJsonObjectFromResponse(response, out var responseObject)) - { - _logger.LogError("Invalid scoresheet-all JSON response."); - return EmptyJsonObject; - } - - return JsonSerializer.Serialize(responseObject, _prettyJsonOptions); - } - - private string NormalizeScoresheetSectionResponse(string response, JsonElement sectionSchemaPayload) - { - if (!TryParseJsonObjectFromResponse(response, out var responseObject)) - { - _logger.LogError("Invalid scoresheet-section JSON response."); - return EmptyJsonObject; - } - - var questionSpecs = BuildSectionQuestionSpecs(sectionSchemaPayload); - var normalized = new Dictionary(); - IEnumerable questionIds = questionSpecs.Count > 0 - ? questionSpecs.Keys - : responseObject.EnumerateObject().Select(p => p.Name); - - foreach (var questionId in questionIds) - { - responseObject.TryGetProperty(questionId, out var value); - var answer = value.ValueKind == JsonValueKind.Undefined ? string.Empty : value.ToString(); - var rationale = string.Empty; - var confidence = 0; - - if (value.ValueKind == JsonValueKind.Object) - { - if (value.TryGetProperty(AIJsonKeys.Answer, out var answerProp)) - { - answer = answerProp.ToString(); - } - - if (value.TryGetProperty(AIJsonKeys.Rationale, out var rationaleProp)) - { - rationale = rationaleProp.ToString(); - } - - if (value.TryGetProperty(AIJsonKeys.Confidence, out var confidenceProp)) - { - confidence = NormalizeConfidenceIncrement(ParseConfidenceValue(confidenceProp)); - } - } - - questionSpecs.TryGetValue(questionId, out var questionSpec); - var normalizedAnswer = NormalizeAnswerByQuestionType(answer, questionSpec); - var normalizedRationale = rationale?.Trim() ?? string.Empty; - var normalizedConfidence = NormalizeConfidenceIncrement(confidence); - - normalized[questionId] = new Dictionary - { - [AIJsonKeys.Answer] = normalizedAnswer, - [AIJsonKeys.Rationale] = normalizedRationale, - [AIJsonKeys.Confidence] = normalizedConfidence - }; - } - - return JsonSerializer.Serialize(normalized, _prettyJsonOptions); - } - - private static Dictionary BuildSectionQuestionSpecs(JsonElement sectionSchemaPayload) - { - var specs = new Dictionary(StringComparer.Ordinal); - foreach (var question in EnumerateSectionQuestions(sectionSchemaPayload)) - { - if (!TryGetQuestionId(question, out var questionId)) - { - continue; - } - - var spec = new SectionQuestionSpec - { - QuestionType = question.TryGetProperty("type", out var typeProp) - ? typeProp.GetString() ?? string.Empty - : string.Empty - }; - - if (question.TryGetProperty("options", out var options) && options.ValueKind == JsonValueKind.Array) - { - foreach (var option in options.EnumerateArray()) - { - if (!option.TryGetProperty("number", out var numberProp)) - { - continue; - } - - var number = numberProp.ValueKind == JsonValueKind.Number - ? numberProp.GetInt32().ToString() - : numberProp.ToString(); - - if (string.IsNullOrWhiteSpace(number)) - { - continue; - } - - spec.OptionNumbers.Add(number); - var label = option.TryGetProperty("value", out var valueProp) ? valueProp.ToString() : string.Empty; - spec.OptionLabels[number] = label ?? string.Empty; - } - } - - specs[questionId] = spec; - } - - return specs; - } - - private static IEnumerable EnumerateSectionQuestions(JsonElement sectionSchemaPayload) - { - if (sectionSchemaPayload.ValueKind == JsonValueKind.Array) - { - foreach (var question in sectionSchemaPayload.EnumerateArray()) - { - if (question.ValueKind == JsonValueKind.Object) - { - yield return question; - } - } - } - else if (sectionSchemaPayload.ValueKind == JsonValueKind.Object && - sectionSchemaPayload.TryGetProperty("questions", out var questions) && - questions.ValueKind == JsonValueKind.Array) - { - foreach (var question in questions.EnumerateArray()) - { - if (question.ValueKind == JsonValueKind.Object) - { - yield return question; - } - } - } - } - - private static bool TryGetQuestionId(JsonElement question, out string questionId) - { - questionId = string.Empty; - if (!question.TryGetProperty("id", out var idProp) || idProp.ValueKind != JsonValueKind.String) - { - return false; - } - - questionId = idProp.GetString() ?? string.Empty; - return !string.IsNullOrWhiteSpace(questionId); - } - - private static object NormalizeAnswerByQuestionType(string answer, SectionQuestionSpec? questionSpec) - { - var normalizedAnswer = answer?.Trim() ?? string.Empty; - var questionType = questionSpec?.QuestionType ?? string.Empty; - - if (questionType.Equals("YesNo", StringComparison.OrdinalIgnoreCase)) - { - if (normalizedAnswer.Equals("Yes", StringComparison.OrdinalIgnoreCase)) - { - return "Yes"; - } - - if (normalizedAnswer.Equals("No", StringComparison.OrdinalIgnoreCase)) - { - return "No"; - } - - return "No"; - } - - if (questionType.Equals("Number", StringComparison.OrdinalIgnoreCase)) - { - if (decimal.TryParse(normalizedAnswer, out var decimalAnswer)) - { - return decimalAnswer; - } - - return 0; - } - - if (questionType.Equals("SelectList", StringComparison.OrdinalIgnoreCase)) - { - return NormalizeSelectListAnswer(normalizedAnswer, questionSpec); - } - - if (questionType.Equals("Text", StringComparison.OrdinalIgnoreCase) || - questionType.Equals("TextArea", StringComparison.OrdinalIgnoreCase)) - { - return normalizedAnswer; - } - - return normalizedAnswer; - } - - private static string NormalizeSelectListAnswer(string answer, SectionQuestionSpec? questionSpec) - { - var options = questionSpec?.OptionNumbers ?? new List(); - if (options.Count == 0) - { - return answer; - } - - if (options.Contains(answer)) - { - return answer; - } - - if (int.TryParse(answer, out var parsedAnswer) && options.Contains(parsedAnswer.ToString())) - { - return parsedAnswer.ToString(); - } - - return answer; - } - - private sealed class SectionQuestionSpec - { - public string QuestionType { get; set; } = string.Empty; - public List OptionNumbers { get; set; } = new(); - public Dictionary OptionLabels { get; set; } = new(StringComparer.OrdinalIgnoreCase); - } - - private static int NormalizeConfidenceIncrement(int confidence) - { - var rounded = (int)Math.Round(confidence / 5.0, MidpointRounding.AwayFromZero) * 5; - return Math.Clamp(rounded, 0, 100); - } - - private static int ParseConfidenceValue(JsonElement confidenceProp) - { - if (confidenceProp.ValueKind == JsonValueKind.Number) - { - if (confidenceProp.TryGetInt32(out var intValue)) - { - return intValue; - } - - if (confidenceProp.TryGetDouble(out var doubleValue)) - { - return (int)Math.Round(doubleValue, MidpointRounding.AwayFromZero); - } - } - - if (confidenceProp.ValueKind == JsonValueKind.String) - { - var raw = confidenceProp.GetString(); - if (int.TryParse(raw, out var parsedInt)) - { - return parsedInt; - } - - if (double.TryParse(raw, out var parsedDouble)) - { - return (int)Math.Round(parsedDouble, MidpointRounding.AwayFromZero); - } - } - - return 0; - } - - private void LogPromptOutput(string promptType, string output) - { - if (!LogPayloads) - { - return; - } - - var formattedOutput = FormatPromptOutputForLog(promptType, output); - _logger.LogDebug( - "AI {PromptType} model output payload: {ModelOutput}", - promptType, - formattedOutput); - WriteAiPromptLog(promptType, "OUTPUT", formattedOutput); - } - - private void LogPromptInput(string promptType, string? systemPrompt, string userPrompt) - { - if (!LogPayloads) - { - return; - } - - var formattedInput = FormatPromptInputForLog(systemPrompt, userPrompt); - _logger.LogDebug( - "AI {PromptType} input payload: {PromptInput}", - promptType, - formattedInput); - WriteAiPromptLog(promptType, "INPUT", formattedInput); - } - - private void WriteAiPromptLog(string promptType, string payloadType, string payload) - { - if (!LogPayloads) - { - return; - } - - try - { - var now = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss zzz"); - var logPath = Path.Combine(AppContext.BaseDirectory, AiPromptLogRelativePath); - EnsureAiPromptLogInitialized(logPath); - - var entry = $"{now} [{promptType}] {payloadType}\n{payload}\n\n"; - File.AppendAllText(logPath, entry); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to write AI prompt log file."); - } - } - - private static void EnsureAiPromptLogInitialized(string logPath) - { - var directory = Path.GetDirectoryName(logPath); - if (!string.IsNullOrWhiteSpace(directory)) - { - Directory.CreateDirectory(directory); - } - - // Reset once per process run so each fresh app run starts with a clean AI prompt log. - if (Interlocked.Exchange(ref _aiPromptLogInitialized, 1) == 0) - { - File.WriteAllText(logPath, string.Empty); - } - } - - private static string FormatPromptInputForLog(string? systemPrompt, string userPrompt) - { - var normalizedSystemPrompt = string.IsNullOrWhiteSpace(systemPrompt) ? string.Empty : systemPrompt.Trim(); - var normalizedUserPrompt = string.IsNullOrWhiteSpace(userPrompt) ? string.Empty : userPrompt.Trim(); - return $"SYSTEM_PROMPT\n{normalizedSystemPrompt}\n\nUSER_PROMPT\n{normalizedUserPrompt}"; - } - - private string FormatPromptOutputForLog(string promptType, string output) - { - if (string.IsNullOrWhiteSpace(output)) - { - return string.Empty; - } - - // For JSON contracts, log only normalized payload JSON. - if (TryParseJsonObjectFromResponse(output, out var jsonObject)) - { - return JsonSerializer.Serialize(jsonObject, _prettyJsonOptions); - } - - return output.Trim(); - } - - private static bool TryParseJsonObjectFromResponse(string response, out JsonElement objectElement) - { - objectElement = default; - var cleaned = CleanJsonResponse(response); - if (string.IsNullOrWhiteSpace(cleaned)) - { - return false; - } - - try - { - using var doc = JsonDocument.Parse(cleaned); - if (doc.RootElement.ValueKind != JsonValueKind.Object) - { - return false; - } - - objectElement = doc.RootElement.Clone(); - return true; - } - catch (JsonException) - { - return false; - } - } - - private static bool IsEmptyJsonObject(string json) - { - if (string.IsNullOrWhiteSpace(json)) - { - return true; - } - - try - { - using var doc = JsonDocument.Parse(json); - return doc.RootElement.ValueKind == JsonValueKind.Object && - !doc.RootElement.EnumerateObject().Any(); - } - catch (JsonException) - { - return true; - } - } - - private async Task ExecutePromptWithRetryAsync( - string promptType, - string systemPrompt, - string userPrompt, - int maxTokens, - Func normalizeResponse, - Func isValidNormalizedResponse, - string fallbackResponse, - int maxAttempts = 2) - { - LogPromptInput(promptType, systemPrompt, userPrompt); - - for (var attempt = 1; attempt <= maxAttempts; attempt++) - { - var rawResponse = await GenerateCompletionAsync(new AICompletionRequest - { - UserPrompt = userPrompt, - SystemPrompt = systemPrompt, - MaxTokens = maxTokens - }); - - var outputType = attempt == 1 ? promptType : $"{promptType}Retry"; - LogPromptOutput(outputType, rawResponse); - - var normalized = normalizeResponse(rawResponse); - if (isValidNormalizedResponse(normalized)) - { - return normalized; - } - - if (attempt < maxAttempts) - { - _logger.LogWarning( - "{PromptType} response failed output-shape validation on attempt {Attempt}/{MaxAttempts}. Retrying.", - promptType, - attempt, - maxAttempts); - } - } - - return fallbackResponse; - } - - private static bool IsValidAnalysisNormalizedResponse(string normalizedJson) - { - if (!TryParseJsonObjectFromResponse(normalizedJson, out var root)) - { - return false; - } - - return root.TryGetProperty("rating", out _) && - root.TryGetProperty("errors", out _) && - root.TryGetProperty("warnings", out _) && - root.TryGetProperty("summaries", out _); - } - - private static bool IsCompleteScoresheetSectionResponse(string normalizedJson, JsonElement sectionSchemaPayload) - { - if (!TryParseJsonObjectFromResponse(normalizedJson, out var root)) - { - return false; - } - - var expectedQuestionIds = EnumerateSectionQuestions(sectionSchemaPayload) - .Select(q => TryGetQuestionId(q, out var id) ? id : string.Empty) - .Where(id => !string.IsNullOrWhiteSpace(id)) - .ToList(); - - if (expectedQuestionIds.Count == 0) - { - return false; - } - - foreach (var questionId in expectedQuestionIds) - { - if (!root.TryGetProperty(questionId, out var answerObject) || answerObject.ValueKind != JsonValueKind.Object) - { - return false; - } - - if (!answerObject.TryGetProperty(AIJsonKeys.Answer, out var answerProp) || - string.IsNullOrWhiteSpace(answerProp.ToString())) - { - return false; - } - - if (!answerObject.TryGetProperty(AIJsonKeys.Rationale, out var rationaleProp) || - string.IsNullOrWhiteSpace(rationaleProp.ToString())) - { - return false; - } - - if (!answerObject.TryGetProperty(AIJsonKeys.Confidence, out _)) - { - return false; - } - } - - return true; - } - - private static string CleanJsonResponse(string response) - { - if (string.IsNullOrWhiteSpace(response)) - { - return string.Empty; - } - - var cleaned = response.Trim(); - - if (cleaned.StartsWith("```json", StringComparison.OrdinalIgnoreCase) || cleaned.StartsWith("```")) - { - var startIndex = cleaned.IndexOf('\n'); - if (startIndex >= 0) - { - cleaned = cleaned.Substring(startIndex + 1); - } - } - - if (cleaned.EndsWith("```", StringComparison.Ordinal)) - { - var lastIndex = cleaned.LastIndexOf("```", StringComparison.Ordinal); - if (lastIndex > 0) - { - cleaned = cleaned.Substring(0, lastIndex); - } - } - - return cleaned.Trim(); - } - } -} - - diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/PromptCoreRules.v1.txt b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/PromptCoreRules.txt similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/PromptCoreRules.v1.txt rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/PromptCoreRules.txt diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/PromptHeader.v1.txt b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/PromptHeader.txt similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/PromptHeader.v1.txt rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/PromptHeader.txt diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/ScoresheetPrompts.v1.txt b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/ScoresheetPrompts.txt similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/ScoresheetPrompts.v1.txt rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v0/ScoresheetPrompts.txt diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v1/AnalysisPrompts.cs.txt b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v1/AnalysisPrompts.cs.txt new file mode 100644 index 0000000000..2e54412806 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v1/AnalysisPrompts.cs.txt @@ -0,0 +1,126 @@ +namespace Unity.GrantManager.AI +{ + internal static class AnalysisPrompts + { + public const string DefaultRubric = @"BC GOVERNMENT GRANT EVALUATION RUBRIC: + +1. ELIGIBILITY REQUIREMENTS: + - Project must align with program objectives + - Applicant must be eligible entity type + - Budget must be reasonable and well-justified + - Project timeline must be realistic + +2. COMPLETENESS CHECKS: + - All required fields completed + - Necessary supporting documents provided + - Budget breakdown detailed and accurate + - Project description clear and comprehensive + +3. FINANCIAL REVIEW: + - Requested amount is within program limits + - Budget is reasonable for scope of work + - Matching funds or in-kind contributions identified + - Cost per outcome/beneficiary is reasonable + +4. RISK ASSESSMENT: + - Applicant capacity to deliver project + - Technical feasibility of proposed work + - Environmental or regulatory compliance + - Potential for cost overruns or delays + +5. QUALITY INDICATORS: + - Clear project objectives and outcomes + - Well-defined target audience/beneficiaries + - Appropriate project methodology + - Sustainability plan for long-term impact + +EVALUATION CRITERIA: +- HIGH: Meets all requirements, well-prepared application, low risk +- MEDIUM: Meets most requirements, minor issues or missing elements +- LOW: Missing key requirements, significant concerns, high risk"; + + public const string ScoreRules = @"HIGH: Application demonstrates strong evidence across most rubric areas with few or no issues. +MEDIUM: Application has some gaps or weaknesses that require reviewer attention. +LOW: Application has significant gaps or risks across key rubric areas."; + + public const string SeverityRules = @"ERROR: Issue that would likely prevent the application from being approved. +WARNING: Issue that could negatively affect the application's approval. +RECOMMENDATION: Reviewer-facing improvement or follow-up consideration."; + + public const string OutputTemplate = @"{ + ""rating"": ""HIGH/MEDIUM/LOW"", + ""warnings"": [ + { + ""title"": ""Brief summary of the warning"", + ""detail"": ""Detailed warning message with full context and explanation"" + } + ], + ""errors"": [ + { + ""title"": ""Brief summary of the error"", + ""detail"": ""Detailed error message with full context and explanation"" + } + ], + ""summaries"": [ + { + ""title"": ""Brief summary of the recommendation"", + ""detail"": ""Detailed recommendation with specific actionable guidance"" + } + ], + ""dismissed"": [] +}"; + + public const string Rules = @"- Use only SCHEMA, DATA, ATTACHMENTS, and RUBRIC as evidence. +- Do not invent fields, documents, requirements, or facts. +- Treat missing or empty values as findings only when they weaken rubric evidence. +- Prefer material issues; avoid nitpicking. +- Each error/warning/recommendation must describe one concrete issue or consideration and why it matters. +- Use 3-6 words for title. +- Each detail must be 1-2 complete sentences. +- Each detail must be grounded in concrete evidence from provided inputs. +- If attachment evidence is used, reference the attachment explicitly in detail. +- Do not provide applicant-facing advice. +- Do not mention rubric section names in findings. +- If no findings exist, return empty arrays. +- rating must be HIGH, MEDIUM, or LOW." + + "\n" + PromptCoreRules.ExactOutputShape + + "\n" + PromptCoreRules.NoExtraOutputKeys + + "\n" + PromptCoreRules.ValidJsonOnly + + "\n" + PromptCoreRules.PlainJsonOnly; + + public static readonly string SystemPrompt = PromptHeader.Build( + "You are an expert grant analyst assistant for human reviewers.", + "Using SCHEMA, DATA, ATTACHMENTS, RUBRIC, SEVERITY, SCORE, OUTPUT, and RULES, return review findings."); + + public static string BuildUserPrompt( + string schemaJson, + string dataJson, + string attachmentsJson, + string rubric) + { + return $@"SCHEMA +{schemaJson} + +DATA +{dataJson} + +ATTACHMENTS +{attachmentsJson} + +RUBRIC +{rubric} + +SEVERITY +{SeverityRules} + +SCORE +{ScoreRules} + +OUTPUT +{OutputTemplate} + +RULES +{Rules}"; + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v1/AttachmentPrompts.cs.txt b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v1/AttachmentPrompts.cs.txt new file mode 100644 index 0000000000..969480ea86 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v1/AttachmentPrompts.cs.txt @@ -0,0 +1,27 @@ +namespace Unity.GrantManager.AI +{ + internal static class AttachmentPrompts + { + public static readonly string SystemPrompt = PromptHeader.Build( + "You are a professional grant analyst for the BC Government.", + "Produce a concise reviewer-facing summary of the provided attachment context."); + + public const string OutputSection = @"OUTPUT +- Plain text only +- 1-2 complete sentences"; + + public const string RulesSection = @"RULES +- Use only the provided attachment context as evidence. +- If text content is present, summarize the actual content. +- If text content is missing or empty, provide a conservative metadata-based summary. +- Do not invent missing details. +- Keep the summary specific, concrete, and reviewer-facing. +- Return plain text only (no markdown, bullets, or JSON)."; + + public static string BuildUserPrompt(string attachmentPayloadJson) + { + return $@"ATTACHMENT +{attachmentPayloadJson}"; + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v1/OpenAIService.cs.txt b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v1/OpenAIService.cs.txt new file mode 100644 index 0000000000..418c31ebcf --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v1/OpenAIService.cs.txt @@ -0,0 +1,833 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace Unity.GrantManager.AI +{ + public class OpenAIService : IAIService, ITransientDependency + { + private readonly HttpClient _httpClient; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + private readonly ITextExtractionService _textExtractionService; + private const string ApplicationAnalysisPromptType = "ApplicationAnalysis"; + private const string AttachmentSummaryPromptType = "AttachmentSummary"; + private const string ScoresheetAllPromptType = "ScoresheetAll"; + private const string ScoresheetSectionPromptType = "ScoresheetSection"; + private const string NoSummaryGeneratedMessage = "No summary generated."; + private const string ServiceNotConfiguredMessage = "AI analysis not available - service not configured."; + private const string ServiceTemporarilyUnavailableMessage = "AI analysis failed - service temporarily unavailable."; + private const string SummaryFailedRetryMessage = "AI analysis failed - please try again later."; + + private string? ApiKey => _configuration["Azure:OpenAI:ApiKey"]; + private string? ApiUrl => _configuration["Azure:OpenAI:ApiUrl"] ?? "https://api.openai.com/v1/chat/completions"; + private readonly string MissingApiKeyMessage = "OpenAI API key is not configured"; + + // Optional local debugging sink for prompt payload logs to a local file. + // Not intended for deployed/shared environments. + private bool IsPromptFileLoggingEnabled => _configuration.GetValue("Azure:Logging:EnablePromptFileLog") ?? false; + private const string PromptLogDirectoryName = "logs"; + private static readonly string PromptLogFileName = $"ai-prompts-{DateTime.UtcNow:yyyyMMdd-HHmmss}-{Environment.ProcessId}.log"; + + private static readonly JsonSerializerOptions JsonLogOptions = new() { WriteIndented = true }; + + public OpenAIService( + HttpClient httpClient, + IConfiguration configuration, + ILogger logger, + ITextExtractionService textExtractionService) + { + _httpClient = httpClient; + _configuration = configuration; + _logger = logger; + _textExtractionService = textExtractionService; + } + + public Task IsAvailableAsync() + { + if (string.IsNullOrEmpty(ApiKey)) + { + _logger.LogWarning("Error: {Message}", MissingApiKeyMessage); + return Task.FromResult(false); + } + + return Task.FromResult(true); + } + + public async Task GenerateCompletionAsync(AICompletionRequest request) + { + var content = await GenerateSummaryAsync( + request?.UserPrompt ?? string.Empty, + request?.SystemPrompt, + request?.MaxTokens ?? 150); + return new AICompletionResponse { Content = content }; + } + + public async Task GenerateApplicationAnalysisAsync(ApplicationAnalysisRequest request) + { + var dataJson = JsonSerializer.Serialize(request.Data, JsonLogOptions); + var schemaJson = JsonSerializer.Serialize(request.Schema, JsonLogOptions); + + var attachmentsPayload = request.Attachments + .Select(a => new + { + name = string.IsNullOrWhiteSpace(a.Name) ? "attachment" : a.Name.Trim(), + summary = string.IsNullOrWhiteSpace(a.Summary) ? string.Empty : a.Summary.Trim() + }) + .Cast(); + + var analysisContent = AnalysisPrompts.BuildUserPrompt( + schemaJson, + dataJson, + JsonSerializer.Serialize(attachmentsPayload, JsonLogOptions), + request.Rubric ?? string.Empty); + + var systemPrompt = AnalysisPrompts.SystemPrompt; + await LogPromptInputAsync(ApplicationAnalysisPromptType, systemPrompt, analysisContent); + var raw = await GenerateSummaryAsync(analysisContent, systemPrompt, 1000); + await LogPromptOutputAsync(ApplicationAnalysisPromptType, raw); + return ParseApplicationAnalysisResponse(AddIdsToAnalysisItems(raw)); + } + + public async Task GenerateSummaryAsync(string content, string? prompt = null, int maxTokens = 150) + { + if (string.IsNullOrEmpty(ApiKey)) + { + _logger.LogWarning("Error: {Message}", MissingApiKeyMessage); + return ServiceNotConfiguredMessage; + } + + _logger.LogDebug("Calling OpenAI chat completions. PromptLength: {PromptLength}, MaxTokens: {MaxTokens}", content?.Length ?? 0, maxTokens); + + try + { + var systemPrompt = prompt ?? "You are a professional grant analyst for the BC Government."; + var userPrompt = content ?? string.Empty; + + var requestBody = new + { + messages = new[] + { + new { role = "system", content = systemPrompt }, + new { role = "user", content = userPrompt } + }, + max_tokens = maxTokens, + temperature = 0.3 + }; + + var json = JsonSerializer.Serialize(requestBody); + var httpContent = new StringContent(json, Encoding.UTF8, "application/json"); + + _httpClient.DefaultRequestHeaders.Clear(); + _httpClient.DefaultRequestHeaders.Add("Authorization", ApiKey); + + var response = await _httpClient.PostAsync(ApiUrl, httpContent); + var responseContent = await response.Content.ReadAsStringAsync(); + + _logger.LogDebug( + "OpenAI chat completions response received. StatusCode: {StatusCode}, ResponseLength: {ResponseLength}", + response.StatusCode, + responseContent?.Length ?? 0); + + if (!response.IsSuccessStatusCode) + { + _logger.LogError("OpenAI API request failed: {StatusCode} - {Content}", response.StatusCode, responseContent); + return ServiceTemporarilyUnavailableMessage; + } + + if (string.IsNullOrWhiteSpace(responseContent)) + { + return NoSummaryGeneratedMessage; + } + + using var jsonDoc = JsonDocument.Parse(responseContent); + var choices = jsonDoc.RootElement.GetProperty("choices"); + if (choices.GetArrayLength() > 0) + { + var message = choices[0].GetProperty("message"); + return message.GetProperty("content").GetString() ?? NoSummaryGeneratedMessage; + } + + return NoSummaryGeneratedMessage; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error generating AI summary"); + return SummaryFailedRetryMessage; + } + } + + public async Task GenerateAttachmentSummaryAsync(string fileName, byte[] fileContent, string contentType) + { + try + { + var extractedText = await _textExtractionService.ExtractTextAsync(fileName, fileContent, contentType); + + var prompt = $@"{AttachmentPrompts.SystemPrompt} + +{AttachmentPrompts.OutputSection} + +{AttachmentPrompts.RulesSection}"; + + var attachmentText = string.IsNullOrWhiteSpace(extractedText) ? null : extractedText; + if (attachmentText != null) + { + _logger.LogDebug("Extracted {TextLength} characters from {FileName}", extractedText.Length, fileName); + } + else + { + _logger.LogDebug("No text extracted from {FileName}, analyzing metadata only", fileName); + } + + var attachmentPayload = new + { + name = fileName, + contentType, + sizeBytes = fileContent.Length, + text = attachmentText + }; + var contentToAnalyze = AttachmentPrompts.BuildUserPrompt( + JsonSerializer.Serialize(attachmentPayload, JsonLogOptions)); + + await LogPromptInputAsync(AttachmentSummaryPromptType, prompt, contentToAnalyze); + var modelOutput = await GenerateSummaryAsync(contentToAnalyze, prompt, 150); + await LogPromptOutputAsync(AttachmentSummaryPromptType, modelOutput); + return modelOutput; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error generating attachment summary for {FileName}", fileName); + return $"AI analysis not available for this attachment ({fileName})."; + } + } + + public async Task GenerateAttachmentSummaryAsync(AttachmentSummaryRequest request) + { + var summary = await GenerateAttachmentSummaryAsync( + request?.FileName ?? string.Empty, + request?.FileContent ?? Array.Empty(), + request?.ContentType ?? "application/octet-stream"); + return new AttachmentSummaryResponse { Summary = summary }; + } + + public async Task AnalyzeApplicationAsync(string applicationContent, List attachmentSummaries, string rubric, string? formFieldConfiguration = null) + { + if (string.IsNullOrEmpty(ApiKey)) + { + _logger.LogWarning("{Message}", MissingApiKeyMessage); + return ServiceNotConfiguredMessage; + } + + try + { + object schemaPayload = new { }; + if (!string.IsNullOrWhiteSpace(formFieldConfiguration)) + { + try + { + using var schemaDoc = JsonDocument.Parse(formFieldConfiguration); + schemaPayload = schemaDoc.RootElement.Clone(); + } + catch (JsonException ex) + { + _logger.LogWarning(ex, "Invalid form field configuration JSON. Using empty schema payload."); + } + } + + var dataPayload = new + { + applicationContent + }; + + var attachmentsPayload = attachmentSummaries?.Count > 0 + ? attachmentSummaries + .Select((summary, index) => new + { + name = $"Attachment {index + 1}", + summary = summary + }) + .Cast() + : Enumerable.Empty(); + + var analysisContent = AnalysisPrompts.BuildUserPrompt( + JsonSerializer.Serialize(schemaPayload, JsonLogOptions), + JsonSerializer.Serialize(dataPayload, JsonLogOptions), + JsonSerializer.Serialize(attachmentsPayload, JsonLogOptions), + rubric); + + var systemPrompt = AnalysisPrompts.SystemPrompt; + + await LogPromptInputAsync(ApplicationAnalysisPromptType, systemPrompt, analysisContent); + var rawAnalysis = await GenerateSummaryAsync(analysisContent, systemPrompt, 1000); + await LogPromptOutputAsync(ApplicationAnalysisPromptType, rawAnalysis); + + // Post-process the AI response to add unique IDs to errors and warnings + return AddIdsToAnalysisItems(rawAnalysis); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error analyzing application"); + return SummaryFailedRetryMessage; + } + } + + private string AddIdsToAnalysisItems(string analysisJson) + { + try + { + using var jsonDoc = JsonDocument.Parse(analysisJson); + using var memoryStream = new System.IO.MemoryStream(); + using (var writer = new Utf8JsonWriter(memoryStream, new JsonWriterOptions { Indented = true })) + { + writer.WriteStartObject(); + + foreach (var property in jsonDoc.RootElement.EnumerateObject()) + { + var outputPropertyName = property.Name; + + if (outputPropertyName == AIJsonKeys.Errors || outputPropertyName == AIJsonKeys.Warnings) + { + writer.WritePropertyName(outputPropertyName); + writer.WriteStartArray(); + + foreach (var item in property.Value.EnumerateArray()) + { + writer.WriteStartObject(); + + // Add unique ID first + writer.WriteString("id", Guid.NewGuid().ToString()); + + // Copy existing properties + foreach (var itemProperty in item.EnumerateObject()) + { + itemProperty.WriteTo(writer); + } + + writer.WriteEndObject(); + } + + writer.WriteEndArray(); + } + else + { + if (outputPropertyName != property.Name) + { + writer.WritePropertyName(outputPropertyName); + property.Value.WriteTo(writer); + continue; + } + + property.WriteTo(writer); + } + } + + // Add dismissed array if not present. + if (!jsonDoc.RootElement.TryGetProperty(AIJsonKeys.Dismissed, out _)) + { + writer.WritePropertyName(AIJsonKeys.Dismissed); + writer.WriteStartArray(); + writer.WriteEndArray(); + } + + writer.WriteEndObject(); + } + + return Encoding.UTF8.GetString(memoryStream.ToArray()); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error adding IDs to analysis items, returning original JSON"); + return analysisJson; // Return original if processing fails + } + } + + public async Task GenerateScoresheetAnswersAsync(string applicationContent, List attachmentSummaries, string scoresheetQuestions) + { + if (string.IsNullOrEmpty(ApiKey)) + { + _logger.LogWarning("{Message}", MissingApiKeyMessage); + return "{}"; + } + + try + { + var attachmentSummariesText = attachmentSummaries?.Count > 0 + ? string.Join("\n- ", attachmentSummaries.Select((s, i) => $"Attachment {i + 1}: {s}")) + : "No attachments provided."; + + var analysisContent = $@"APPLICATION CONTENT: +{applicationContent} + +ATTACHMENT SUMMARIES: +- {attachmentSummariesText} + +SCORESHEET QUESTIONS: +{scoresheetQuestions} + +Please analyze this grant application and provide appropriate answers for each scoresheet question. + +For numeric questions, provide a numeric value within the specified range. +For yes/no questions, provide either 'Yes' or 'No'. +For text questions, provide a concise, relevant response. +For select list questions, choose the most appropriate option from the provided choices. +For text area questions, provide a detailed but concise response. + +Base your answers on the application content and attachment summaries provided. Be objective and fair in your assessment. + +Return your response as a JSON object where each key is the question ID and the value is the appropriate answer: +{{ + ""question-id-1"": ""answer-value-1"", + ""question-id-2"": ""answer-value-2"" +}} +Do not return any markdown formatting, just the JSON by itself"; + + var systemPrompt = @"You are an expert grant application reviewer for the BC Government. +Analyze the provided application and generate appropriate answers for the scoresheet questions based on the application content. +Be thorough, objective, and fair in your assessment. Base your answers strictly on the provided application content. +Respond only with valid JSON in the exact format requested."; + + await LogPromptInputAsync(ScoresheetAllPromptType, systemPrompt, analysisContent); + var modelOutput = await GenerateSummaryAsync(analysisContent, systemPrompt, 2000); + await LogPromptOutputAsync(ScoresheetAllPromptType, modelOutput); + return modelOutput; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error generating scoresheet answers"); + return "{}"; + } + } + + public async Task GenerateScoresheetSectionAnswersAsync(string applicationContent, List attachmentSummaries, string sectionJson, string sectionName) + { + if (string.IsNullOrEmpty(ApiKey)) + { + _logger.LogWarning("{Message}", MissingApiKeyMessage); + return "{}"; + } + + try + { + var attachmentSummariesText = attachmentSummaries?.Count > 0 + ? string.Join("\n- ", attachmentSummaries.Select((s, i) => $"Attachment {i + 1}: {s}")) + : "No attachments provided."; + + object sectionQuestionsPayload = sectionJson; + if (!string.IsNullOrWhiteSpace(sectionJson)) + { + try + { + using var sectionDoc = JsonDocument.Parse(sectionJson); + sectionQuestionsPayload = sectionDoc.RootElement.Clone(); + } + catch (JsonException) + { + // Keep raw string payload when JSON parsing fails. + } + } + + var sectionPayload = new + { + name = sectionName, + questions = sectionQuestionsPayload + }; + var sectionPayloadJson = JsonSerializer.Serialize(sectionPayload, JsonLogOptions); + var responseTemplate = BuildScoresheetSectionResponseTemplate(sectionPayloadJson); + + var analysisContent = ScoresheetPrompts.BuildSectionUserPrompt( + applicationContent, + attachmentSummariesText, + sectionPayloadJson, + responseTemplate); + + var systemPrompt = ScoresheetPrompts.SectionSystemPrompt; + + await LogPromptInputAsync(ScoresheetSectionPromptType, systemPrompt, analysisContent); + var modelOutput = await GenerateSummaryAsync(analysisContent, systemPrompt, 2000); + await LogPromptOutputAsync(ScoresheetSectionPromptType, modelOutput); + return modelOutput; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error generating scoresheet section answers for section {SectionName}", sectionName); + return "{}"; + } + } + + public async Task GenerateScoresheetSectionAnswersAsync(ScoresheetSectionRequest request) + { + var dataJson = JsonSerializer.Serialize(request.Data, JsonLogOptions); + var sectionJson = JsonSerializer.Serialize(request.SectionSchema, JsonLogOptions); + + var attachmentSummaries = request.Attachments + .Select(a => $"{a.Name}: {a.Summary}") + .ToList(); + + var raw = await GenerateScoresheetSectionAnswersAsync( + dataJson, + attachmentSummaries, + sectionJson, + request.SectionName); + return ParseScoresheetSectionResponse(raw); + } + + private static ApplicationAnalysisResponse ParseApplicationAnalysisResponse(string raw) + { + var response = new ApplicationAnalysisResponse(); + + if (!TryParseJsonObjectFromResponse(raw, out var root)) + { + return response; + } + + if (TryGetStringProperty(root, AIJsonKeys.Rating, out var rating)) + { + response.Rating = rating; + } + + if (root.TryGetProperty("errors", out var errors) && errors.ValueKind == JsonValueKind.Array) + { + response.Errors = ParseFindings(errors); + } + + if (root.TryGetProperty("warnings", out var warnings) && warnings.ValueKind == JsonValueKind.Array) + { + response.Warnings = ParseFindings(warnings); + } + + if (root.TryGetProperty(AIJsonKeys.Summaries, out var summaries) && summaries.ValueKind == JsonValueKind.Array) + { + response.Summaries = ParseFindings(summaries); + } + + if (root.TryGetProperty(AIJsonKeys.Dismissed, out var dismissed) && dismissed.ValueKind == JsonValueKind.Array) + { + response.Dismissed = dismissed + .EnumerateArray() + .Select(GetStringValueOrNull) + .Where(item => !string.IsNullOrWhiteSpace(item)) + .Cast() + .ToList(); + } + + return response; + } + + private static bool TryGetStringProperty(JsonElement root, string propertyName, out string? value) + { + value = null; + if (!root.TryGetProperty(propertyName, out var property) || property.ValueKind != JsonValueKind.String) + { + return false; + } + + value = property.GetString(); + return !string.IsNullOrWhiteSpace(value); + } + + private static string? GetStringValueOrNull(JsonElement element) + { + if (element.ValueKind == JsonValueKind.String) + { + return element.GetString(); + } + + return null; + } + + private static List ParseFindings(JsonElement array) + { + var findings = new List(); + foreach (var item in array.EnumerateArray()) + { + if (item.ValueKind != JsonValueKind.Object) + { + continue; + } + + var id = item.TryGetProperty(AIJsonKeys.Id, out var idProp) && idProp.ValueKind == JsonValueKind.String + ? idProp.GetString() + : null; + string? title = null; + if (item.TryGetProperty(AIJsonKeys.Title, out var titleProp) && titleProp.ValueKind == JsonValueKind.String) + { + title = titleProp.GetString(); + } + else if (item.TryGetProperty("category", out var legacyTitleProp) && + legacyTitleProp.ValueKind == JsonValueKind.String) + { + title = legacyTitleProp.GetString(); + } + + string? detail = null; + if (item.TryGetProperty(AIJsonKeys.Detail, out var detailProp) && detailProp.ValueKind == JsonValueKind.String) + { + detail = detailProp.GetString(); + } + else if (item.TryGetProperty("message", out var legacyDetailProp) && + legacyDetailProp.ValueKind == JsonValueKind.String) + { + detail = legacyDetailProp.GetString(); + } + + findings.Add(new ApplicationAnalysisFinding + { + Id = id, + Title = title, + Detail = detail + }); + } + + return findings; + } + + private static ScoresheetSectionResponse ParseScoresheetSectionResponse(string raw) + { + var response = new ScoresheetSectionResponse(); + if (!TryParseJsonObjectFromResponse(raw, out var root)) + { + return response; + } + + foreach (var property in root.EnumerateObject()) + { + if (property.Value.ValueKind != JsonValueKind.Object) + { + continue; + } + + var answer = property.Value.TryGetProperty("answer", out var answerProp) + ? answerProp.Clone() + : default; + var rationale = property.Value.TryGetProperty("rationale", out var rationaleProp) && + rationaleProp.ValueKind == JsonValueKind.String + ? rationaleProp.GetString() ?? string.Empty + : string.Empty; + var confidence = property.Value.TryGetProperty("confidence", out var confidenceProp) && + confidenceProp.ValueKind == JsonValueKind.Number && + confidenceProp.TryGetInt32(out var parsedConfidence) + ? NormalizeConfidence(parsedConfidence) + : 0; + + response.Answers[property.Name] = new ScoresheetSectionAnswer + { + Answer = answer, + Rationale = rationale, + Confidence = confidence + }; + } + + return response; + } + + private static int NormalizeConfidence(int confidence) + { + var clamped = Math.Clamp(confidence, 0, 100); + var rounded = (int)Math.Round(clamped / 5.0, MidpointRounding.AwayFromZero) * 5; + return Math.Clamp(rounded, 0, 100); + } + + private static string BuildScoresheetSectionResponseTemplate(string sectionPayloadJson) + { + try + { + using var doc = JsonDocument.Parse(sectionPayloadJson); + if (!doc.RootElement.TryGetProperty("questions", out var questions) || questions.ValueKind != JsonValueKind.Array) + { + return ScoresheetPrompts.SectionOutputTemplate; + } + + var template = new Dictionary(); + foreach (var question in questions.EnumerateArray()) + { + if (!question.TryGetProperty("id", out var idProp) || idProp.ValueKind != JsonValueKind.String) + { + continue; + } + + var questionId = idProp.GetString(); + if (string.IsNullOrWhiteSpace(questionId)) + { + continue; + } + + template[questionId] = new + { + answer = string.Empty, + rationale = string.Empty, + confidence = 0 + }; + } + + if (template.Count == 0) + { + return ScoresheetPrompts.SectionOutputTemplate; + } + + return JsonSerializer.Serialize(template, JsonLogOptions); + } + catch (JsonException) + { + return ScoresheetPrompts.SectionOutputTemplate; + } + } + + private async Task LogPromptInputAsync(string promptType, string? systemPrompt, string userPrompt) + { + var formattedInput = FormatPromptInputForLog(systemPrompt, userPrompt); + _logger.LogInformation("AI {PromptType} input payload: {PromptInput}", promptType, formattedInput); + await WritePromptLogFileAsync(promptType, "INPUT", formattedInput); + } + + private async Task LogPromptOutputAsync(string promptType, string output) + { + var formattedOutput = FormatPromptOutputForLog(output); + _logger.LogInformation("AI {PromptType} model output payload: {ModelOutput}", promptType, formattedOutput); + await WritePromptLogFileAsync(promptType, "OUTPUT", formattedOutput); + } + + private async Task WritePromptLogFileAsync(string promptType, string payloadType, string payload) + { + if (!CanWritePromptFileLog()) + { + return; + } + + try + { + var now = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss zzz"); + var logDirectory = Path.Combine(AppContext.BaseDirectory, PromptLogDirectoryName); + Directory.CreateDirectory(logDirectory); + + var logPath = Path.Combine(logDirectory, PromptLogFileName); + var entry = $"{now} [{promptType}] {payloadType}\n{payload}\n\n"; + await File.AppendAllTextAsync(logPath, entry); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to write AI prompt log file."); + } + } + + private bool CanWritePromptFileLog() + { + return IsPromptFileLoggingEnabled; + } + + private static string FormatPromptInputForLog(string? systemPrompt, string userPrompt) + { + var normalizedSystemPrompt = string.IsNullOrWhiteSpace(systemPrompt) ? string.Empty : systemPrompt.Trim(); + var normalizedUserPrompt = string.IsNullOrWhiteSpace(userPrompt) ? string.Empty : userPrompt.Trim(); + return $"SYSTEM_PROMPT\n{normalizedSystemPrompt}\n\nUSER_PROMPT\n{normalizedUserPrompt}"; + } + + private static string FormatPromptOutputForLog(string output) + { + if (string.IsNullOrWhiteSpace(output)) + { + return string.Empty; + } + + if (TryParseJsonObjectFromResponse(output, out var jsonObject)) + { + return JsonSerializer.Serialize(jsonObject, JsonLogOptions); + } + + return output.Trim(); + } + + private static bool TryParseJsonObjectFromResponse(string response, out JsonElement objectElement) + { + objectElement = default; + var cleaned = CleanJsonResponse(response); + if (string.IsNullOrWhiteSpace(cleaned)) + { + return false; + } + + try + { + using var doc = JsonDocument.Parse(cleaned); + if (doc.RootElement.ValueKind != JsonValueKind.Object) + { + return false; + } + + objectElement = doc.RootElement.Clone(); + return true; + } + catch (JsonException) + { + return false; + } + } + + private static string CleanJsonResponse(string response) + { + if (string.IsNullOrWhiteSpace(response)) + { + return string.Empty; + } + + var cleaned = response.Trim(); + + if (cleaned.StartsWith("```json", StringComparison.OrdinalIgnoreCase) || cleaned.StartsWith("```")) + { + var startIndex = cleaned.IndexOf('\n'); + if (startIndex >= 0) + { + // Multi-line fenced code block: remove everything up to and including the first newline. + cleaned = cleaned[(startIndex + 1)..]; + } + else + { + // Single-line fenced JSON, e.g. ```json { ... } ``` or ```{ ... } ```. + // Strip everything before the first likely JSON payload token. + var jsonStart = FindFirstJsonTokenIndex(cleaned); + + if (jsonStart > 0) + { + cleaned = cleaned[jsonStart..]; + } + } + } + + if (cleaned.EndsWith("```", StringComparison.Ordinal)) + { + var lastIndex = cleaned.LastIndexOf("```", StringComparison.Ordinal); + if (lastIndex > 0) + { + cleaned = cleaned[..lastIndex]; + } + } + + return cleaned.Trim(); + } + + private static int FindFirstJsonTokenIndex(string value) + { + var objectStart = value.IndexOf('{'); + var arrayStart = value.IndexOf('['); + + if (objectStart >= 0 && arrayStart >= 0) + { + return Math.Min(objectStart, arrayStart); + } + + if (objectStart >= 0) + { + return objectStart; + } + + return arrayStart; + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v1/PromptCoreRules.cs.txt b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v1/PromptCoreRules.cs.txt new file mode 100644 index 0000000000..e11dce3c97 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v1/PromptCoreRules.cs.txt @@ -0,0 +1,13 @@ +namespace Unity.GrantManager.AI +{ + internal static class PromptCoreRules + { + public const string UseProvidedEvidence = "- Use only provided input sections as evidence."; + public const string NoInvention = "- Do not invent missing details."; + public const string MinimumNarrativeWords = "- Any narrative text response must be at least 12 words."; + public const string ExactOutputShape = "- Return values exactly as specified in OUTPUT."; + public const string NoExtraOutputKeys = "- Do not return keys outside OUTPUT."; + public const string ValidJsonOnly = "- Return valid JSON only."; + public const string PlainJsonOnly = "- Return plain JSON only (no markdown)."; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v1/PromptHeader.cs.txt b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v1/PromptHeader.cs.txt new file mode 100644 index 0000000000..701a43e740 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v1/PromptHeader.cs.txt @@ -0,0 +1,14 @@ +namespace Unity.GrantManager.AI +{ + internal static class PromptHeader + { + public static string Build(string role, string task) + { + return $@"ROLE +{role} + +TASK +{task}"; + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v1/ScoresheetPrompts.cs.txt b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v1/ScoresheetPrompts.cs.txt new file mode 100644 index 0000000000..2db4de742d --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Baselines/v1/ScoresheetPrompts.cs.txt @@ -0,0 +1,80 @@ +namespace Unity.GrantManager.AI +{ + internal static class ScoresheetPrompts + { + public static readonly string SectionSystemPrompt = PromptHeader.Build( + "You are an expert grant application reviewer for the BC Government.", + "Using DATA, ATTACHMENTS, SECTION, RESPONSE, OUTPUT, and RULES, answer only the questions in SECTION."); + + public const string SectionOutputTemplate = @"{ + """": { + ""answer"": """", + ""rationale"": """", + ""confidence"": + } +}"; + + public const string SectionRules = "- Use only DATA and ATTACHMENTS as evidence.\n" + + "- Do not invent missing application details.\n" + + @"- Return exactly one answer object per question ID in SECTION.questions. +- Do not omit any question IDs from SECTION.questions. +- Do not add keys that are not question IDs from SECTION.questions. +- Use RESPONSE as the output contract and fill every placeholder value. +- Follow this process in order: (1) copy RESPONSE, (2) iterate SECTION.questions in order, (3) fill answer+rationale+confidence for each matching question ID, (4) run final completeness check. +- Each answer object must include: ""answer"", ""rationale"", and ""confidence"". +- Never omit ""answer"", ""rationale"", or ""confidence"" for any question type. +- The ""answer"" value type must match question type: Number => numeric; YesNo/SelectList/Text/TextArea => string. +- The ""rationale"" field must be 1-2 complete sentences and grounded in concrete DATA/ATTACHMENTS evidence. +- In ""rationale"", cite concrete source evidence from the provided input content; do not cite prompt section headers. +- For every question, rationale must justify both the selected answer and the selected confidence level based on evidence strength. +- If explicit evidence is insufficient, choose the most conservative valid answer and state uncertainty in rationale. +- Do not treat missing or non-contradictory information as evidence. +- The ""confidence"" field must be an integer from 0 to 100 in increments of 5 and represents confidence in the selected answer. +- Set confidence by certainty of the selected answer based on available evidence, regardless of which option is selected. +- For yes/no questions, the ""answer"" field must be exactly ""Yes"" or ""No"". +- For numeric questions, answer must be a numeric value within the allowed range. +- For numeric questions, answer must never be blank. +- If evidence is insufficient for a numeric question, return the minimum allowed numeric value and explain uncertainty in rationale. +- If a required value is explicitly missing in DATA/ATTACHMENTS, set confidence high (80-100) when selecting the conservative minimum. +- For select list questions, return only the selected options.number as a string (the option index shown in options), never label text or points. +- For select list questions, the ""answer"" value must be one of question.allowed_answers exactly. +- Never return 0 for select list answers unless 0 exists as an explicit option number. +- For text and text area questions, answer must be concise, evidence-based, non-empty, and avoid boilerplate placeholders. +- For text and text area questions, answer is the reviewer comment, and rationale must explain the evidence basis and certainty for that comment. +- For comment fields, summarize key evidence-based conclusions from the other questions in SECTION, including uncertainty where applicable. +- Do not leave rationale empty when answer is populated. +- Final self-check before responding: every question ID in RESPONSE must have a non-empty ""answer"", non-empty ""rationale"", and ""confidence"". +- If any answer object is incomplete, regenerate the full JSON response before returning it. +" + + PromptCoreRules.MinimumNarrativeWords + "\n" + + PromptCoreRules.ExactOutputShape + "\n" + + PromptCoreRules.NoExtraOutputKeys + "\n" + + PromptCoreRules.ValidJsonOnly + "\n" + + PromptCoreRules.PlainJsonOnly; + + public static string BuildSectionUserPrompt( + string applicationContent, + string attachmentSummariesText, + string sectionPayloadJson, + string responseTemplateJson) + { + return $@"DATA +{applicationContent} + +ATTACHMENTS +- {attachmentSummariesText} + +SECTION +{sectionPayloadJson} + +RESPONSE +{responseTemplateJson} + +OUTPUT +{SectionOutputTemplate} + +RULES +{SectionRules}"; + } + } +} From 979e7b97ed1c14866bd6069dfdf661c3e4caa0fb Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 6 Mar 2026 16:58:18 -0800 Subject: [PATCH 09/58] AB#32009 Add runtime prompt version selector with single-file v0/v1 profiles --- .../AI/OpenAIService.cs | 78 ++++++++++----- .../AI/Prompts/AnalysisPrompts.cs | 98 ++++++++++++++++--- .../AI/Prompts/AttachmentPrompts.cs | 32 ++++++ .../AI/Prompts/ScoresheetPrompts.cs | 43 ++++++++ 4 files changed, 211 insertions(+), 40 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs index 418c31ebcf..9c1ecd11a1 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs @@ -22,6 +22,8 @@ public class OpenAIService : IAIService, ITransientDependency private const string AttachmentSummaryPromptType = "AttachmentSummary"; private const string ScoresheetAllPromptType = "ScoresheetAll"; private const string ScoresheetSectionPromptType = "ScoresheetSection"; + private const string PromptVersionV0 = "v0"; + private const string PromptVersionV1 = "v1"; private const string NoSummaryGeneratedMessage = "No summary generated."; private const string ServiceNotConfiguredMessage = "AI analysis not available - service not configured."; private const string ServiceTemporarilyUnavailableMessage = "AI analysis failed - service temporarily unavailable."; @@ -39,6 +41,9 @@ public class OpenAIService : IAIService, ITransientDependency private static readonly JsonSerializerOptions JsonLogOptions = new() { WriteIndented = true }; + private string SelectedPromptVersion => NormalizePromptVersion(_configuration["Azure:OpenAI:PromptVersion"]); + private bool UseV0Prompts => string.Equals(SelectedPromptVersion, PromptVersionV0, StringComparison.OrdinalIgnoreCase); + public OpenAIService( HttpClient httpClient, IConfiguration configuration, @@ -84,13 +89,10 @@ public async Task GenerateApplicationAnalysisAsync( }) .Cast(); - var analysisContent = AnalysisPrompts.BuildUserPrompt( - schemaJson, - dataJson, - JsonSerializer.Serialize(attachmentsPayload, JsonLogOptions), - request.Rubric ?? string.Empty); - - var systemPrompt = AnalysisPrompts.SystemPrompt; + var attachmentsJson = JsonSerializer.Serialize(attachmentsPayload, JsonLogOptions); + var rubric = request.Rubric ?? AnalysisPrompts.GetRubric(UseV0Prompts); + var analysisContent = AnalysisPrompts.BuildUserPrompt(schemaJson, dataJson, attachmentsJson, rubric, UseV0Prompts); + var systemPrompt = AnalysisPrompts.GetSystemPrompt(UseV0Prompts); await LogPromptInputAsync(ApplicationAnalysisPromptType, systemPrompt, analysisContent); var raw = await GenerateSummaryAsync(analysisContent, systemPrompt, 1000); await LogPromptOutputAsync(ApplicationAnalysisPromptType, raw); @@ -171,11 +173,11 @@ public async Task GenerateAttachmentSummaryAsync(string fileName, byte[] { var extractedText = await _textExtractionService.ExtractTextAsync(fileName, fileContent, contentType); - var prompt = $@"{AttachmentPrompts.SystemPrompt} + var prompt = $@"{AttachmentPrompts.GetSystemPrompt(UseV0Prompts)} -{AttachmentPrompts.OutputSection} +{AttachmentPrompts.GetOutputSection(UseV0Prompts)} -{AttachmentPrompts.RulesSection}"; +{AttachmentPrompts.GetRulesSection(UseV0Prompts)}"; var attachmentText = string.IsNullOrWhiteSpace(extractedText) ? null : extractedText; if (attachmentText != null) @@ -194,13 +196,13 @@ public async Task GenerateAttachmentSummaryAsync(string fileName, byte[] sizeBytes = fileContent.Length, text = attachmentText }; - var contentToAnalyze = AttachmentPrompts.BuildUserPrompt( - JsonSerializer.Serialize(attachmentPayload, JsonLogOptions)); + var payloadJson = JsonSerializer.Serialize(attachmentPayload, JsonLogOptions); + var contentToAnalyze = AttachmentPrompts.BuildUserPrompt(payloadJson, UseV0Prompts); await LogPromptInputAsync(AttachmentSummaryPromptType, prompt, contentToAnalyze); var modelOutput = await GenerateSummaryAsync(contentToAnalyze, prompt, 150); await LogPromptOutputAsync(AttachmentSummaryPromptType, modelOutput); - return modelOutput; + return UseV0Prompts ? ExtractSummaryFromJson(modelOutput) : modelOutput; } catch (Exception ex) { @@ -257,13 +259,14 @@ public async Task AnalyzeApplicationAsync(string applicationContent, Lis .Cast() : Enumerable.Empty(); - var analysisContent = AnalysisPrompts.BuildUserPrompt( - JsonSerializer.Serialize(schemaPayload, JsonLogOptions), - JsonSerializer.Serialize(dataPayload, JsonLogOptions), - JsonSerializer.Serialize(attachmentsPayload, JsonLogOptions), - rubric); - - var systemPrompt = AnalysisPrompts.SystemPrompt; + var schemaJson = JsonSerializer.Serialize(schemaPayload, JsonLogOptions); + var dataJson = JsonSerializer.Serialize(dataPayload, JsonLogOptions); + var attachmentsJson = JsonSerializer.Serialize(attachmentsPayload, JsonLogOptions); + var fallbackRubric = string.IsNullOrWhiteSpace(rubric) + ? AnalysisPrompts.GetRubric(UseV0Prompts) + : rubric; + var analysisContent = AnalysisPrompts.BuildUserPrompt(schemaJson, dataJson, attachmentsJson, fallbackRubric, UseV0Prompts); + var systemPrompt = AnalysisPrompts.GetSystemPrompt(UseV0Prompts); await LogPromptInputAsync(ApplicationAnalysisPromptType, systemPrompt, analysisContent); var rawAnalysis = await GenerateSummaryAsync(analysisContent, systemPrompt, 1000); @@ -446,9 +449,10 @@ public async Task GenerateScoresheetSectionAnswersAsync(string applicati applicationContent, attachmentSummariesText, sectionPayloadJson, - responseTemplate); + responseTemplate, + UseV0Prompts); - var systemPrompt = ScoresheetPrompts.SectionSystemPrompt; + var systemPrompt = ScoresheetPrompts.GetSectionSystemPrompt(UseV0Prompts); await LogPromptInputAsync(ScoresheetSectionPromptType, systemPrompt, analysisContent); var modelOutput = await GenerateSummaryAsync(analysisContent, systemPrompt, 2000); @@ -683,14 +687,14 @@ private static string BuildScoresheetSectionResponseTemplate(string sectionPaylo private async Task LogPromptInputAsync(string promptType, string? systemPrompt, string userPrompt) { var formattedInput = FormatPromptInputForLog(systemPrompt, userPrompt); - _logger.LogInformation("AI {PromptType} input payload: {PromptInput}", promptType, formattedInput); + _logger.LogInformation("AI {PromptType} ({PromptVersion}) input payload: {PromptInput}", promptType, SelectedPromptVersion, formattedInput); await WritePromptLogFileAsync(promptType, "INPUT", formattedInput); } private async Task LogPromptOutputAsync(string promptType, string output) { var formattedOutput = FormatPromptOutputForLog(output); - _logger.LogInformation("AI {PromptType} model output payload: {ModelOutput}", promptType, formattedOutput); + _logger.LogInformation("AI {PromptType} ({PromptVersion}) model output payload: {ModelOutput}", promptType, SelectedPromptVersion, formattedOutput); await WritePromptLogFileAsync(promptType, "OUTPUT", formattedOutput); } @@ -829,5 +833,31 @@ private static int FindFirstJsonTokenIndex(string value) return arrayStart; } + + private static string NormalizePromptVersion(string? version) + { + if (string.Equals(version, PromptVersionV0, StringComparison.OrdinalIgnoreCase)) + { + return PromptVersionV0; + } + + return PromptVersionV1; + } + + private static string ExtractSummaryFromJson(string output) + { + if (!TryParseJsonObjectFromResponse(output, out var jsonObject)) + { + return output?.Trim() ?? string.Empty; + } + + if (jsonObject.TryGetProperty(AIJsonKeys.Summary, out var summaryProp) && + summaryProp.ValueKind == JsonValueKind.String) + { + return summaryProp.GetString() ?? string.Empty; + } + + return output?.Trim() ?? string.Empty; + } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/AnalysisPrompts.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/AnalysisPrompts.cs index 2e54412806..97841e760a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/AnalysisPrompts.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/AnalysisPrompts.cs @@ -2,6 +2,12 @@ namespace Unity.GrantManager.AI { internal static class AnalysisPrompts { + public const string DefaultRubricV0 = @"ELIGIBILITY REQUIREMENTS: Project aligns with program objectives; Applicant is an eligible entity; Budget is reasonable and justified; Timeline is realistic. +COMPLETENESS CHECKS: Required information is present; Supporting materials are provided where applicable; Description is clear. +FINANCIAL REVIEW: Requested amount is within limits; Budget matches scope; Matching funds or contributions are identified. +RISK ASSESSMENT: Applicant capacity; Feasibility; Compliance considerations; Delivery risks. +QUALITY INDICATORS: Clear objectives; Defined beneficiaries; Appropriate approach; Long-term sustainability."; + public const string DefaultRubric = @"BC GOVERNMENT GRANT EVALUATION RUBRIC: 1. ELIGIBILITY REQUIREMENTS: @@ -43,6 +49,45 @@ internal static class AnalysisPrompts MEDIUM: Application has some gaps or weaknesses that require reviewer attention. LOW: Application has significant gaps or risks across key rubric areas."; + public const string OutputTemplateV0 = @"{ + ""rating"": """", + ""errors"": [ + { + ""title"": """", + ""detail"": """" + } + ], + ""warnings"": [ + { + ""title"": """", + ""detail"": """" + } + ], + ""summaries"": [ + { + ""title"": """", + ""detail"": """" + } + ] +}"; + + public const string RulesV0 = PromptCoreRules.UseProvidedEvidence + "\n" + + "- Do not invent fields, documents, requirements, or facts.\n" + + @"- Treat missing or empty values as findings only when they weaken rubric evidence. +- Prefer material issues; avoid nitpicking. +- Use 3-6 words for title. +- Each detail must be 1-2 complete sentences. +- Each detail must cite concrete evidence from DATA or ATTACHMENTS. +- If ATTACHMENTS evidence is used, cite the attachment by name in detail. +- If no findings exist, return empty arrays. +- Rating must be HIGH, MEDIUM, or LOW. +" + + PromptCoreRules.MinimumNarrativeWords + "\n" + + PromptCoreRules.ExactOutputShape + "\n" + + PromptCoreRules.NoExtraOutputKeys + "\n" + + PromptCoreRules.ValidJsonOnly + "\n" + + PromptCoreRules.PlainJsonOnly; + public const string SeverityRules = @"ERROR: Issue that would likely prevent the application from being approved. WARNING: Issue that could negatively affect the application's approval. RECOMMENDATION: Reviewer-facing improvement or follow-up consideration."; @@ -51,20 +96,20 @@ internal static class AnalysisPrompts ""rating"": ""HIGH/MEDIUM/LOW"", ""warnings"": [ { - ""title"": ""Brief summary of the warning"", - ""detail"": ""Detailed warning message with full context and explanation"" + ""category"": ""Brief summary of the warning"", + ""message"": ""Detailed warning message with full context and explanation"" } ], ""errors"": [ { - ""title"": ""Brief summary of the error"", - ""detail"": ""Detailed error message with full context and explanation"" + ""category"": ""Brief summary of the error"", + ""message"": ""Detailed error message with full context and explanation"" } ], ""summaries"": [ { - ""title"": ""Brief summary of the recommendation"", - ""detail"": ""Detailed recommendation with specific actionable guidance"" + ""category"": ""Brief summary of the recommendation"", + ""message"": ""Detailed recommendation with specific actionable guidance"" } ], ""dismissed"": [] @@ -75,10 +120,10 @@ internal static class AnalysisPrompts - Treat missing or empty values as findings only when they weaken rubric evidence. - Prefer material issues; avoid nitpicking. - Each error/warning/recommendation must describe one concrete issue or consideration and why it matters. -- Use 3-6 words for title. -- Each detail must be 1-2 complete sentences. -- Each detail must be grounded in concrete evidence from provided inputs. -- If attachment evidence is used, reference the attachment explicitly in detail. +- Use 3-6 words for category. +- Each message must be 1-2 complete sentences. +- Each message must be grounded in concrete evidence from provided inputs. +- If attachment evidence is used, reference the attachment explicitly in the message. - Do not provide applicant-facing advice. - Do not mention rubric section names in findings. - If no findings exist, return empty arrays. @@ -92,12 +137,36 @@ internal static class AnalysisPrompts "You are an expert grant analyst assistant for human reviewers.", "Using SCHEMA, DATA, ATTACHMENTS, RUBRIC, SEVERITY, SCORE, OUTPUT, and RULES, return review findings."); + public static readonly string SystemPromptV0 = PromptHeader.Build( + "You are an expert grant analyst assistant for human reviewers.", + "Using SCHEMA, DATA, ATTACHMENTS, RUBRIC, SCORE, OUTPUT, and RULES, return review findings."); + + public static string GetRubric(bool useV0) => useV0 ? DefaultRubricV0 : DefaultRubric; + public static string GetSystemPrompt(bool useV0) => useV0 ? SystemPromptV0 : SystemPrompt; + public static string BuildUserPrompt( string schemaJson, string dataJson, string attachmentsJson, string rubric) { + return BuildUserPrompt(schemaJson, dataJson, attachmentsJson, rubric, useV0: false); + } + + public static string BuildUserPrompt( + string schemaJson, + string dataJson, + string attachmentsJson, + string rubric, + bool useV0) + { + var output = useV0 ? OutputTemplateV0 : OutputTemplate; + var rules = useV0 ? RulesV0 : Rules; + var severitySection = useV0 ? string.Empty : $@"SEVERITY +{SeverityRules} + +"; + return $@"SCHEMA {schemaJson} @@ -110,17 +179,14 @@ public static string BuildUserPrompt( RUBRIC {rubric} -SEVERITY -{SeverityRules} - -SCORE +{severitySection}SCORE {ScoreRules} OUTPUT -{OutputTemplate} +{output} RULES -{Rules}"; +{rules}"; } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/AttachmentPrompts.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/AttachmentPrompts.cs index 969480ea86..dd950b2065 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/AttachmentPrompts.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/AttachmentPrompts.cs @@ -6,10 +6,19 @@ internal static class AttachmentPrompts "You are a professional grant analyst for the BC Government.", "Produce a concise reviewer-facing summary of the provided attachment context."); + public static readonly string SystemPromptV0 = PromptHeader.Build( + "You are a professional grant analyst for the BC Government.", + "Produce a concise reviewer-facing summary of the provided attachment context."); + public const string OutputSection = @"OUTPUT - Plain text only - 1-2 complete sentences"; + public const string OutputSectionV0 = @"OUTPUT +{ + ""summary"": """" +}"; + public const string RulesSection = @"RULES - Use only the provided attachment context as evidence. - If text content is present, summarize the actual content. @@ -18,7 +27,30 @@ internal static class AttachmentPrompts - Keep the summary specific, concrete, and reviewer-facing. - Return plain text only (no markdown, bullets, or JSON)."; + public const string RulesSectionV0 = "- Use only ATTACHMENT as evidence.\n" + + "- If ATTACHMENT.text is present, summarize actual content.\n" + + "- If ATTACHMENT.text is null or empty, provide a conservative file-level summary.\n" + + PromptCoreRules.NoInvention + "\n" + + @"- Write 1-2 complete sentences. +- Summary must be grounded in concrete ATTACHMENT evidence. +- Return exactly one object with only the key: summary. +" + + PromptCoreRules.MinimumNarrativeWords + "\n" + + PromptCoreRules.ExactOutputShape + "\n" + + PromptCoreRules.NoExtraOutputKeys + "\n" + + PromptCoreRules.ValidJsonOnly + "\n" + + PromptCoreRules.PlainJsonOnly; + + public static string GetSystemPrompt(bool useV0) => useV0 ? SystemPromptV0 : SystemPrompt; + public static string GetOutputSection(bool useV0) => useV0 ? OutputSectionV0 : OutputSection; + public static string GetRulesSection(bool useV0) => useV0 ? RulesSectionV0 : RulesSection; + public static string BuildUserPrompt(string attachmentPayloadJson) + { + return BuildUserPrompt(attachmentPayloadJson, useV0: false); + } + + public static string BuildUserPrompt(string attachmentPayloadJson, bool useV0) { return $@"ATTACHMENT {attachmentPayloadJson}"; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/ScoresheetPrompts.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/ScoresheetPrompts.cs index 2db4de742d..306e43e360 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/ScoresheetPrompts.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/ScoresheetPrompts.cs @@ -2,10 +2,41 @@ namespace Unity.GrantManager.AI { internal static class ScoresheetPrompts { + public static readonly string AllSystemPromptV0 = PromptHeader.Build( + "You are an expert grant application reviewer for the BC Government.", + "Using DATA, ATTACHMENTS, QUESTIONS, OUTPUT, and RULES, provide answers for all scoresheet questions."); + + public const string AllOutputTemplateV0 = @"{ + """": """" +}"; + + public const string AllRulesV0 = "- Use only DATA and ATTACHMENTS as evidence.\n" + + "- Do not invent missing application details.\n" + + @"- Return exactly one answer per question ID in QUESTIONS. +- Do not omit any question IDs from QUESTIONS. +- Do not add keys that are not question IDs from QUESTIONS. +- The ""answer"" value type must match the question type. +- For numeric questions, return a numeric value within the allowed range. +- For yes/no questions, return exactly ""Yes"" or ""No"". +- For select list questions, return only the selected options.number as a string and never return option label text. +- For text and text area questions, return concise, evidence-based text. +- For text and text area questions, include concise source-grounded rationale from the provided input content. +- If explicit evidence is insufficient, choose the most conservative valid answer. +" + + PromptCoreRules.MinimumNarrativeWords + "\n" + + PromptCoreRules.ExactOutputShape + "\n" + + PromptCoreRules.NoExtraOutputKeys + "\n" + + PromptCoreRules.ValidJsonOnly + "\n" + + PromptCoreRules.PlainJsonOnly; + public static readonly string SectionSystemPrompt = PromptHeader.Build( "You are an expert grant application reviewer for the BC Government.", "Using DATA, ATTACHMENTS, SECTION, RESPONSE, OUTPUT, and RULES, answer only the questions in SECTION."); + public static readonly string SectionSystemPromptV0 = PromptHeader.Build( + "You are an expert grant application reviewer for the BC Government.", + "Using DATA, ATTACHMENTS, SECTION, RESPONSE, OUTPUT, and RULES, answer only the questions in SECTION."); + public const string SectionOutputTemplate = @"{ """": { ""answer"": """", @@ -52,11 +83,23 @@ internal static class ScoresheetPrompts + PromptCoreRules.ValidJsonOnly + "\n" + PromptCoreRules.PlainJsonOnly; + public static string GetSectionSystemPrompt(bool useV0) => useV0 ? SectionSystemPromptV0 : SectionSystemPrompt; + public static string BuildSectionUserPrompt( string applicationContent, string attachmentSummariesText, string sectionPayloadJson, string responseTemplateJson) + { + return BuildSectionUserPrompt(applicationContent, attachmentSummariesText, sectionPayloadJson, responseTemplateJson, useV0: false); + } + + public static string BuildSectionUserPrompt( + string applicationContent, + string attachmentSummariesText, + string sectionPayloadJson, + string responseTemplateJson, + bool useV0) { return $@"DATA {applicationContent} From 003dea3c108699aaa07ebd615529ecce3acde343 Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Fri, 6 Mar 2026 17:32:58 -0800 Subject: [PATCH 10/58] AB#322238 fixing missing snapshot --- .../20260307012926_Fixsnaphot.Designer.cs | 2694 +++++++++++++++++ .../20260307012926_Fixsnaphot.cs | 21 + .../GrantManagerDbContextModelSnapshot.cs | 101 +- 3 files changed, 2800 insertions(+), 16 deletions(-) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260307012926_Fixsnaphot.Designer.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260307012926_Fixsnaphot.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260307012926_Fixsnaphot.Designer.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260307012926_Fixsnaphot.Designer.cs new file mode 100644 index 0000000000..bd71f649e4 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260307012926_Fixsnaphot.Designer.cs @@ -0,0 +1,2694 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Unity.GrantManager.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +#nullable disable + +namespace Unity.GrantManager.Migrations.HostMigrations +{ + [DbContext(typeof(GrantManagerDbContext))] + [Migration("20260307012926_Fixsnaphot")] + partial class Fixsnaphot + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.PostgreSql) + .HasAnnotation("ProductVersion", "9.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_blob_triggers", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("qrtz_calendars", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_cron_triggers", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_j_req_recovery"); + + b.ToTable("qrtz_job_details", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("qrtz_locks", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("qrtz_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("qrtz_scheduler_state", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_simple_triggers", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("smallint") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_qrtz_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_qrtz_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_qrtz_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("qrtz_triggers", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applicants.ApplicantTenantMap", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastUpdated") + .HasColumnType("timestamp without time zone"); + + b.Property("OidcSubUsername") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TenantName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OidcSubUsername"); + + b.HasIndex("OidcSubUsername", "TenantId") + .IsUnique(); + + b.ToTable("ApplicantTenantMaps", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Integrations.CasClientCode", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClientCode") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)"); + + b.Property("ClientId") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FinancialMinistry") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastUpdatedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("MinistryPrefix") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)"); + + b.HasKey("Id"); + + b.ToTable("CasClientCodes", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Integrations.DynamicUrl", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("KeyName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("DynamicUrls", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Locality.Community", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegionalDistrictCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Communities", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Locality.EconomicRegion", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("EconomicRegionCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("EconomicRegionName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.HasKey("Id"); + + b.ToTable("EconomicRegions", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Locality.ElectoralDistrict", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ElectoralDistrictCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ElectoralDistrictName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.HasKey("Id"); + + b.ToTable("ElectoralDistricts", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Locality.RegionalDistrict", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("EconomicRegionCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("RegionalDistrictCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegionalDistrictName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("RegionalDistricts", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Locality.Sector", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("SectorCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SectorName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Sectors", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Locality.SubSector", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("SectorId") + .HasColumnType("uuid"); + + b.Property("SubSectorCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SubSectorName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("SectorId"); + + b.ToTable("SubSectors", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Tokens.TenantToken", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("TenantTokens", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApplicationName") + .HasMaxLength(96) + .HasColumnType("character varying(96)") + .HasColumnName("ApplicationName"); + + b.Property("BrowserInfo") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("BrowserInfo"); + + b.Property("ClientId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("ClientId"); + + b.Property("ClientIpAddress") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("ClientIpAddress"); + + b.Property("ClientName") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("ClientName"); + + b.Property("Comments") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("Comments"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("CorrelationId"); + + b.Property("Exceptions") + .HasColumnType("text"); + + b.Property("ExecutionDuration") + .HasColumnType("integer") + .HasColumnName("ExecutionDuration"); + + b.Property("ExecutionTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("HttpMethod") + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasColumnName("HttpMethod"); + + b.Property("HttpStatusCode") + .HasColumnType("integer") + .HasColumnName("HttpStatusCode"); + + b.Property("ImpersonatorTenantId") + .HasColumnType("uuid") + .HasColumnName("ImpersonatorTenantId"); + + b.Property("ImpersonatorTenantName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("ImpersonatorTenantName"); + + b.Property("ImpersonatorUserId") + .HasColumnType("uuid") + .HasColumnName("ImpersonatorUserId"); + + b.Property("ImpersonatorUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("ImpersonatorUserName"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TenantName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("TenantName"); + + b.Property("Url") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("Url"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("UserName"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ExecutionTime"); + + b.HasIndex("TenantId", "UserId", "ExecutionTime"); + + b.ToTable("AuditLogs", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.AuditLogAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuditLogId") + .HasColumnType("uuid") + .HasColumnName("AuditLogId"); + + b.Property("ExecutionDuration") + .HasColumnType("integer") + .HasColumnName("ExecutionDuration"); + + b.Property("ExecutionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("ExecutionTime"); + + b.Property("ExtraProperties") + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("MethodName") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("MethodName"); + + b.Property("Parameters") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("Parameters"); + + b.Property("ServiceName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("ServiceName"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AuditLogId"); + + b.HasIndex("TenantId", "ServiceName", "MethodName", "ExecutionTime"); + + b.ToTable("AuditLogActions", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.EntityChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuditLogId") + .HasColumnType("uuid") + .HasColumnName("AuditLogId"); + + b.Property("ChangeTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("ChangeTime"); + + b.Property("ChangeType") + .HasColumnType("smallint") + .HasColumnName("ChangeType"); + + b.Property("EntityId") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("EntityId"); + + b.Property("EntityTenantId") + .HasColumnType("uuid"); + + b.Property("EntityTypeFullName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("EntityTypeFullName"); + + b.Property("ExtraProperties") + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AuditLogId"); + + b.HasIndex("TenantId", "EntityTypeFullName", "EntityId"); + + b.ToTable("EntityChanges", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.EntityPropertyChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EntityChangeId") + .HasColumnType("uuid"); + + b.Property("NewValue") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("NewValue"); + + b.Property("OriginalValue") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("OriginalValue"); + + b.Property("PropertyName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("PropertyName"); + + b.Property("PropertyTypeFullName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("PropertyTypeFullName"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("EntityChangeId"); + + b.ToTable("EntityPropertyChanges", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.BackgroundJobs.BackgroundJobRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsAbandoned") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JobArgs") + .IsRequired() + .HasMaxLength(1048576) + .HasColumnType("character varying(1048576)"); + + b.Property("JobName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("LastTryTime") + .HasColumnType("timestamp without time zone"); + + b.Property("NextTryTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Priority") + .ValueGeneratedOnAdd() + .HasColumnType("smallint") + .HasDefaultValue((byte)15); + + b.Property("TryCount") + .ValueGeneratedOnAdd() + .HasColumnType("smallint") + .HasDefaultValue((short)0); + + b.HasKey("Id"); + + b.HasIndex("IsAbandoned", "NextTryTime"); + + b.ToTable("BackgroundJobs", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.FeatureManagement.FeatureDefinitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AllowedProviders") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DefaultValue") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExtraProperties") + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("IsAvailableToHost") + .HasColumnType("boolean"); + + b.Property("IsVisibleToClients") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ParentName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ValueType") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.HasKey("Id"); + + b.HasIndex("GroupName"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Features", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.FeatureManagement.FeatureGroupDefinitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExtraProperties") + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("FeatureGroups", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.FeatureManagement.FeatureValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ProviderKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ProviderName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.HasKey("Id"); + + b.HasIndex("Name", "ProviderName", "ProviderKey") + .IsUnique(); + + b.ToTable("FeatureValues", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityClaimType", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsStatic") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Regex") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("RegexDescription") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Required") + .HasColumnType("boolean"); + + b.Property("ValueType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ClaimTypes", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityLinkUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("SourceTenantId") + .HasColumnType("uuid"); + + b.Property("SourceUserId") + .HasColumnType("uuid"); + + b.Property("TargetTenantId") + .HasColumnType("uuid"); + + b.Property("TargetUserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("SourceUserId", "SourceTenantId", "TargetUserId", "TargetTenantId") + .IsUnique(); + + b.ToTable("LinkUsers", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("EntityVersion") + .HasColumnType("integer"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDefault") + .HasColumnType("boolean") + .HasColumnName("IsDefault"); + + b.Property("IsPublic") + .HasColumnType("boolean") + .HasColumnName("IsPublic"); + + b.Property("IsStatic") + .HasColumnType("boolean") + .HasColumnName("IsStatic"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName"); + + b.ToTable("Roles", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClaimType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ClaimValue") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("RoleClaims", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentitySecurityLog", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Action") + .HasMaxLength(96) + .HasColumnType("character varying(96)"); + + b.Property("ApplicationName") + .HasMaxLength(96) + .HasColumnType("character varying(96)"); + + b.Property("BrowserInfo") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("ClientId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ClientIpAddress") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("Identity") + .HasMaxLength(96) + .HasColumnType("character varying(96)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TenantName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Action"); + + b.HasIndex("TenantId", "ApplicationName"); + + b.HasIndex("TenantId", "Identity"); + + b.HasIndex("TenantId", "UserId"); + + b.ToTable("SecurityLogs", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentitySession", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClientId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Device") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("DeviceInfo") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ExtraProperties") + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IpAddresses") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("LastAccessed") + .HasColumnType("timestamp without time zone"); + + b.Property("SessionId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("SignedIn") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Device"); + + b.HasIndex("SessionId"); + + b.HasIndex("TenantId", "UserId"); + + b.ToTable("Sessions", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessFailedCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("Email"); + + b.Property("EmailConfirmed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("EmailConfirmed"); + + b.Property("EntityVersion") + .HasColumnType("integer"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("IsActive"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsExternal") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsExternal"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastPasswordChangeTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LockoutEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("LockoutEnabled"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("Name"); + + b.Property("NormalizedEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("NormalizedEmail"); + + b.Property("NormalizedUserName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("NormalizedUserName"); + + b.Property("OidcSub") + .HasColumnType("text"); + + b.Property("PasswordHash") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("PasswordHash"); + + b.Property("PhoneNumber") + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasColumnName("PhoneNumber"); + + b.Property("PhoneNumberConfirmed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("PhoneNumberConfirmed"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("SecurityStamp"); + + b.Property("ShouldChangePasswordOnNextLogin") + .HasColumnType("boolean"); + + b.Property("Surname") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("Surname"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TwoFactorEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("TwoFactorEnabled"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("UserName"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("NormalizedEmail"); + + b.HasIndex("NormalizedUserName"); + + b.HasIndex("UserName"); + + b.ToTable("Users", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClaimType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ClaimValue") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserDelegation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("EndTime") + .HasColumnType("timestamp without time zone"); + + b.Property("SourceUserId") + .HasColumnType("uuid"); + + b.Property("StartTime") + .HasColumnType("timestamp without time zone"); + + b.Property("TargetUserId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("UserDelegations", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserLogin", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("LoginProvider") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ProviderDisplayName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ProviderKey") + .IsRequired() + .HasMaxLength(196) + .HasColumnType("character varying(196)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("UserId", "LoginProvider"); + + b.HasIndex("LoginProvider", "ProviderKey"); + + b.ToTable("UserLogins", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserOrganizationUnit", b => + { + b.Property("OrganizationUnitId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("OrganizationUnitId", "UserId"); + + b.HasIndex("UserId", "OrganizationUnitId"); + + b.ToTable("UserOrganizationUnits", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId", "UserId"); + + b.ToTable("UserRoles", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("LoginProvider") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Name") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("UserTokens", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnit", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(95) + .HasColumnType("character varying(95)") + .HasColumnName("Code"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("DisplayName"); + + b.Property("EntityVersion") + .HasColumnType("integer"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ParentId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("Code"); + + b.HasIndex("ParentId"); + + b.ToTable("OrganizationUnits", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnitRole", b => + { + b.Property("OrganizationUnitId") + .HasColumnType("uuid"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("OrganizationUnitId", "RoleId"); + + b.HasIndex("RoleId", "OrganizationUnitId"); + + b.ToTable("OrganizationUnitRoles", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.PermissionManagement.PermissionDefinitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExtraProperties") + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("MultiTenancySide") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ParentName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Providers") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("StateCheckers") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("GroupName"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Permissions", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.PermissionManagement.PermissionGrant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ProviderKey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ProviderName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Name", "ProviderName", "ProviderKey") + .IsUnique(); + + b.ToTable("PermissionGrants", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.PermissionManagement.PermissionGroupDefinitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExtraProperties") + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("PermissionGroups", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.SettingManagement.Setting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ProviderKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ProviderName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.HasKey("Id"); + + b.HasIndex("Name", "ProviderName", "ProviderKey") + .IsUnique(); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.SettingManagement.SettingDefinitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DefaultValue") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExtraProperties") + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsEncrypted") + .HasColumnType("boolean"); + + b.Property("IsInherited") + .HasColumnType("boolean"); + + b.Property("IsVisibleToClients") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Providers") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("SettingDefinitions", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.TenantManagement.Tenant", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EntityVersion") + .HasColumnType("integer"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.HasIndex("NormalizedName"); + + b.ToTable("Tenants", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.TenantManagement.TenantConnectionString", b => + { + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Name") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("TenantId", "Name"); + + b.ToTable("TenantConnectionStrings", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("Unity.GrantManager.Locality.SubSector", b => + { + b.HasOne("Unity.GrantManager.Locality.Sector", "Sector") + .WithMany("SubSectors") + .HasForeignKey("SectorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Sector"); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.AuditLogAction", b => + { + b.HasOne("Volo.Abp.AuditLogging.AuditLog", null) + .WithMany("Actions") + .HasForeignKey("AuditLogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.EntityChange", b => + { + b.HasOne("Volo.Abp.AuditLogging.AuditLog", null) + .WithMany("EntityChanges") + .HasForeignKey("AuditLogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.EntityPropertyChange", b => + { + b.HasOne("Volo.Abp.AuditLogging.EntityChange", null) + .WithMany("PropertyChanges") + .HasForeignKey("EntityChangeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityRoleClaim", b => + { + b.HasOne("Volo.Abp.Identity.IdentityRole", null) + .WithMany("Claims") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserClaim", b => + { + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("Claims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserLogin", b => + { + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("Logins") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserOrganizationUnit", b => + { + b.HasOne("Volo.Abp.Identity.OrganizationUnit", null) + .WithMany() + .HasForeignKey("OrganizationUnitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("OrganizationUnits") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserRole", b => + { + b.HasOne("Volo.Abp.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("Roles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserToken", b => + { + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("Tokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnit", b => + { + b.HasOne("Volo.Abp.Identity.OrganizationUnit", null) + .WithMany() + .HasForeignKey("ParentId"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnitRole", b => + { + b.HasOne("Volo.Abp.Identity.OrganizationUnit", null) + .WithMany("Roles") + .HasForeignKey("OrganizationUnitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Volo.Abp.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.TenantManagement.TenantConnectionString", b => + { + b.HasOne("Volo.Abp.TenantManagement.Tenant", null) + .WithMany("ConnectionStrings") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); + + modelBuilder.Entity("Unity.GrantManager.Locality.Sector", b => + { + b.Navigation("SubSectors"); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.AuditLog", b => + { + b.Navigation("Actions"); + + b.Navigation("EntityChanges"); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.EntityChange", b => + { + b.Navigation("PropertyChanges"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityRole", b => + { + b.Navigation("Claims"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUser", b => + { + b.Navigation("Claims"); + + b.Navigation("Logins"); + + b.Navigation("OrganizationUnits"); + + b.Navigation("Roles"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnit", b => + { + b.Navigation("Roles"); + }); + + modelBuilder.Entity("Volo.Abp.TenantManagement.Tenant", b => + { + b.Navigation("ConnectionStrings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260307012926_Fixsnaphot.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260307012926_Fixsnaphot.cs new file mode 100644 index 0000000000..74899e427c --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260307012926_Fixsnaphot.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Unity.GrantManager.Migrations.HostMigrations +{ + /// + public partial class Fixsnaphot : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/GrantManagerDbContextModelSnapshot.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/GrantManagerDbContextModelSnapshot.cs index f4cf613605..d034c14e8e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/GrantManagerDbContextModelSnapshot.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/GrantManagerDbContextModelSnapshot.cs @@ -472,16 +472,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Id") .HasColumnType("uuid"); - b.Property("ClientCode") - .IsRequired() - .HasColumnType("text"); - - b.Property("ClientId") - .HasColumnType("text"); - b.Property("ConcurrencyStamp") + .IsConcurrencyToken() .IsRequired() - .HasColumnType("text"); + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); b.Property("CreationTime") .HasColumnType("timestamp without time zone") @@ -491,15 +487,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid") .HasColumnName("CreatorId"); - b.Property("Description") + b.Property("ExtraProperties") .IsRequired() - .HasColumnType("text"); - - b.Property("FinancialMinistry") - .HasColumnType("text"); - - b.Property("IsActive") - .HasColumnType("boolean"); + .HasColumnType("text") + .HasColumnName("ExtraProperties"); b.Property("LastUpdated") .HasColumnType("timestamp without time zone"); @@ -525,6 +516,84 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("ApplicantTenantMaps", (string)null); }); + modelBuilder.Entity("Unity.GrantManager.Integrations.CasClientCode", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClientCode") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)"); + + b.Property("ClientId") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FinancialMinistry") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastUpdatedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("MinistryPrefix") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)"); + + b.HasKey("Id"); + + b.ToTable("CasClientCodes", (string)null); + }); + modelBuilder.Entity("Unity.GrantManager.Integrations.DynamicUrl", b => { b.Property("Id") From 73e791f56c954b5619cff8a9b1911d0dfaf33080 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 6 Mar 2026 18:31:04 -0800 Subject: [PATCH 11/58] AB#32009 Align v1 prompt contracts and preserve v0 path --- .../AI/Prompts/AnalysisPrompts.cs | 20 +++++++------- .../AI/Prompts/AttachmentPrompts.cs | 26 +++++++++---------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/AnalysisPrompts.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/AnalysisPrompts.cs index 97841e760a..6cb16655c3 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/AnalysisPrompts.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/AnalysisPrompts.cs @@ -2,13 +2,13 @@ namespace Unity.GrantManager.AI { internal static class AnalysisPrompts { - public const string DefaultRubricV0 = @"ELIGIBILITY REQUIREMENTS: Project aligns with program objectives; Applicant is an eligible entity; Budget is reasonable and justified; Timeline is realistic. + public const string DefaultRubric = @"ELIGIBILITY REQUIREMENTS: Project aligns with program objectives; Applicant is an eligible entity; Budget is reasonable and justified; Timeline is realistic. COMPLETENESS CHECKS: Required information is present; Supporting materials are provided where applicable; Description is clear. FINANCIAL REVIEW: Requested amount is within limits; Budget matches scope; Matching funds or contributions are identified. RISK ASSESSMENT: Applicant capacity; Feasibility; Compliance considerations; Delivery risks. QUALITY INDICATORS: Clear objectives; Defined beneficiaries; Appropriate approach; Long-term sustainability."; - public const string DefaultRubric = @"BC GOVERNMENT GRANT EVALUATION RUBRIC: + public const string DefaultRubricV0 = @"BC GOVERNMENT GRANT EVALUATION RUBRIC: 1. ELIGIBILITY REQUIREMENTS: - Project must align with program objectives @@ -49,7 +49,7 @@ internal static class AnalysisPrompts MEDIUM: Application has some gaps or weaknesses that require reviewer attention. LOW: Application has significant gaps or risks across key rubric areas."; - public const string OutputTemplateV0 = @"{ + public const string OutputTemplate = @"{ ""rating"": """", ""errors"": [ { @@ -71,7 +71,7 @@ internal static class AnalysisPrompts ] }"; - public const string RulesV0 = PromptCoreRules.UseProvidedEvidence + "\n" + public const string Rules = PromptCoreRules.UseProvidedEvidence + "\n" + "- Do not invent fields, documents, requirements, or facts.\n" + @"- Treat missing or empty values as findings only when they weaken rubric evidence. - Prefer material issues; avoid nitpicking. @@ -92,7 +92,7 @@ internal static class AnalysisPrompts WARNING: Issue that could negatively affect the application's approval. RECOMMENDATION: Reviewer-facing improvement or follow-up consideration."; - public const string OutputTemplate = @"{ + public const string OutputTemplateV0 = @"{ ""rating"": ""HIGH/MEDIUM/LOW"", ""warnings"": [ { @@ -115,7 +115,7 @@ internal static class AnalysisPrompts ""dismissed"": [] }"; - public const string Rules = @"- Use only SCHEMA, DATA, ATTACHMENTS, and RUBRIC as evidence. + public const string RulesV0 = @"- Use only SCHEMA, DATA, ATTACHMENTS, and RUBRIC as evidence. - Do not invent fields, documents, requirements, or facts. - Treat missing or empty values as findings only when they weaken rubric evidence. - Prefer material issues; avoid nitpicking. @@ -135,11 +135,11 @@ internal static class AnalysisPrompts public static readonly string SystemPrompt = PromptHeader.Build( "You are an expert grant analyst assistant for human reviewers.", - "Using SCHEMA, DATA, ATTACHMENTS, RUBRIC, SEVERITY, SCORE, OUTPUT, and RULES, return review findings."); + "Using SCHEMA, DATA, ATTACHMENTS, RUBRIC, SCORE, OUTPUT, and RULES, return review findings."); public static readonly string SystemPromptV0 = PromptHeader.Build( "You are an expert grant analyst assistant for human reviewers.", - "Using SCHEMA, DATA, ATTACHMENTS, RUBRIC, SCORE, OUTPUT, and RULES, return review findings."); + "Using SCHEMA, DATA, ATTACHMENTS, RUBRIC, SEVERITY, SCORE, OUTPUT, and RULES, return review findings."); public static string GetRubric(bool useV0) => useV0 ? DefaultRubricV0 : DefaultRubric; public static string GetSystemPrompt(bool useV0) => useV0 ? SystemPromptV0 : SystemPrompt; @@ -162,10 +162,10 @@ public static string BuildUserPrompt( { var output = useV0 ? OutputTemplateV0 : OutputTemplate; var rules = useV0 ? RulesV0 : Rules; - var severitySection = useV0 ? string.Empty : $@"SEVERITY + var severitySection = useV0 ? $@"SEVERITY {SeverityRules} -"; +" : string.Empty; return $@"SCHEMA {schemaJson} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/AttachmentPrompts.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/AttachmentPrompts.cs index dd950b2065..6e83ea6a13 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/AttachmentPrompts.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/AttachmentPrompts.cs @@ -11,23 +11,11 @@ internal static class AttachmentPrompts "Produce a concise reviewer-facing summary of the provided attachment context."); public const string OutputSection = @"OUTPUT -- Plain text only -- 1-2 complete sentences"; - - public const string OutputSectionV0 = @"OUTPUT { ""summary"": """" }"; - public const string RulesSection = @"RULES -- Use only the provided attachment context as evidence. -- If text content is present, summarize the actual content. -- If text content is missing or empty, provide a conservative metadata-based summary. -- Do not invent missing details. -- Keep the summary specific, concrete, and reviewer-facing. -- Return plain text only (no markdown, bullets, or JSON)."; - - public const string RulesSectionV0 = "- Use only ATTACHMENT as evidence.\n" + public const string RulesSection = "- Use only ATTACHMENT as evidence.\n" + "- If ATTACHMENT.text is present, summarize actual content.\n" + "- If ATTACHMENT.text is null or empty, provide a conservative file-level summary.\n" + PromptCoreRules.NoInvention + "\n" @@ -41,6 +29,18 @@ internal static class AttachmentPrompts + PromptCoreRules.ValidJsonOnly + "\n" + PromptCoreRules.PlainJsonOnly; + public const string OutputSectionV0 = @"OUTPUT +- Plain text only +- 1-2 complete sentences"; + + public const string RulesSectionV0 = @"RULES +- Use only the provided attachment context as evidence. +- If text content is present, summarize the actual content. +- If text content is missing or empty, provide a conservative metadata-based summary. +- Do not invent missing details. +- Keep the summary specific, concrete, and reviewer-facing. +- Return plain text only (no markdown, bullets, or JSON)."; + public static string GetSystemPrompt(bool useV0) => useV0 ? SystemPromptV0 : SystemPrompt; public static string GetOutputSection(bool useV0) => useV0 ? OutputSectionV0 : OutputSection; public static string GetRulesSection(bool useV0) => useV0 ? RulesSectionV0 : RulesSection; From c0e25bdf5ffd3e603fe4846268edca877d59e955 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 6 Mar 2026 18:49:26 -0800 Subject: [PATCH 12/58] AB#32009 Fix async action authorization loop and centralize extractor dispatch --- .../AI/TextExtractionService.cs | 57 +++++++++++-------- .../GrantApplicationAppService.cs | 10 ++-- 2 files changed, 37 insertions(+), 30 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/TextExtractionService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/TextExtractionService.cs index 9aad041a5e..e2353ca796 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/TextExtractionService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/TextExtractionService.cs @@ -23,10 +23,22 @@ public partial class TextExtractionService : ITextExtractionService, ITransientD private const int MaxDocxTableRows = 2000; private const int MaxDocxTableCellsPerRow = 50; private readonly ILogger _logger; + private readonly Dictionary> _extractorsByExtension; public TextExtractionService(ILogger logger) { _logger = logger; + _extractorsByExtension = new Dictionary>(StringComparer.OrdinalIgnoreCase) + { + [".txt"] = (_, content) => ExtractTextFromTextFile(content), + [".csv"] = (_, content) => ExtractTextFromTextFile(content), + [".json"] = (_, content) => ExtractTextFromTextFile(content), + [".xml"] = (_, content) => ExtractTextFromTextFile(content), + [".pdf"] = ExtractTextFromPdfFile, + [".docx"] = (name, content) => ExtractTextFromWordDocx(name, content), + [".xls"] = ExtractTextFromExcelFile, + [".xlsx"] = ExtractTextFromExcelFile + }; } public Task ExtractTextAsync(string fileName, byte[] fileContent, string contentType) @@ -42,46 +54,41 @@ public Task ExtractTextAsync(string fileName, byte[] fileContent, string var normalizedContentType = contentType?.ToLowerInvariant() ?? string.Empty; var extension = Path.GetExtension(fileName)?.ToLowerInvariant() ?? string.Empty; - string rawText; + if (extension == ".doc") + { + _logger.LogDebug("Legacy .doc extraction is not supported for {FileName}", fileName); + return Task.FromResult(string.Empty); + } + + if (_extractorsByExtension.TryGetValue(extension, out var extractor)) + { + var rawText = extractor(fileName, fileContent); + return Task.FromResult(NormalizeAndLimitText(rawText, fileName)); + } - if (normalizedContentType.Contains("text/") || - extension == ".txt" || - extension == ".csv" || - extension == ".json" || - extension == ".xml") + if (normalizedContentType.Contains("text/")) { - rawText = ExtractTextFromTextFile(fileContent); + var rawText = ExtractTextFromTextFile(fileContent); return Task.FromResult(NormalizeAndLimitText(rawText, fileName)); } - if (normalizedContentType.Contains("pdf") || extension == ".pdf") + if (normalizedContentType.Contains("pdf")) { - rawText = ExtractTextFromPdfFile(fileName, fileContent); + var rawText = ExtractTextFromPdfFile(fileName, fileContent); return Task.FromResult(NormalizeAndLimitText(rawText, fileName)); } if (normalizedContentType.Contains("word") || normalizedContentType.Contains("msword") || - normalizedContentType.Contains("officedocument.wordprocessingml") || - extension == ".doc" || - extension == ".docx") + normalizedContentType.Contains("officedocument.wordprocessingml")) { - if (extension == ".docx" || normalizedContentType.Contains("officedocument.wordprocessingml")) - { - rawText = ExtractTextFromWordDocx(fileName, fileContent); - return Task.FromResult(NormalizeAndLimitText(rawText, fileName)); - } - - _logger.LogDebug("Legacy .doc extraction is not supported for {FileName}", fileName); - return Task.FromResult(string.Empty); + var rawText = ExtractTextFromWordDocx(fileName, fileContent); + return Task.FromResult(NormalizeAndLimitText(rawText, fileName)); } - if (normalizedContentType.Contains("excel") || - normalizedContentType.Contains("spreadsheet") || - extension == ".xls" || - extension == ".xlsx") + if (normalizedContentType.Contains("excel") || normalizedContentType.Contains("spreadsheet")) { - rawText = ExtractTextFromExcelFile(fileName, fileContent); + var rawText = ExtractTextFromExcelFile(fileName, fileContent); return Task.FromResult(NormalizeAndLimitText(rawText, fileName)); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs index 075d568663..f6c4943e86 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs @@ -974,11 +974,11 @@ public async Task> GetActions(Guid applicati // NOTE: Authorization is applied on the AppService layer and is false by default // AUTHORIZATION HANDLING - actionDtos.ForEach(async item => - { - item.IsPermitted = item.IsPermitted && (await AuthorizationService.IsGrantedAsync(application, GetActionAuthorizationRequirement(item.ApplicationAction))); - item.IsAuthorized = true; - }); + foreach (var item in actionDtos) + { + item.IsPermitted = item.IsPermitted && (await AuthorizationService.IsGrantedAsync(application, GetActionAuthorizationRequirement(item.ApplicationAction))); + item.IsAuthorized = true; + } return new ListResultDto(actionDtos); } From 377ceac4a057c762a02b3ee6bda57170a6d9a25f Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Mon, 9 Mar 2026 11:39:06 -0700 Subject: [PATCH 13/58] feature/AB#31884 - Remove "Title, Role" placeholder text on assessments --- .../Views/Shared/Components/UserInfoWidget/Default.cshtml | 1 - 1 file changed, 1 deletion(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/UserInfoWidget/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/UserInfoWidget/Default.cshtml index 4e7e5b93fd..1618cdc453 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/UserInfoWidget/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/UserInfoWidget/Default.cshtml @@ -12,7 +12,6 @@
@Model.Badge
@Model.DisplayName
-
@Model.Title
From 27a73a676ae5d07d68c373c1b8aadae70e018060 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Mon, 9 Mar 2026 11:51:27 -0700 Subject: [PATCH 14/58] feature/AB#32221 - Rename default sort column "Created Date" in Applicants table --- .../src/Unity.GrantManager.Web/Pages/Applicants/Index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Index.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Index.js index 0ff7065093..88fed040f6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Index.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Index.js @@ -376,7 +376,7 @@ $(function () { function getCreationTimeColumn(columnIndex) { return { - title: 'Creation Date', + title: 'Created Date', data: 'creationTime', name: 'creationTime', className: 'data-table-header', From 05f3a4a61903c5b60d7a202f9af2fec4cfdda4d7 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Mon, 9 Mar 2026 12:30:41 -0700 Subject: [PATCH 15/58] feature/AB#31961 - Render Applicant Name as Hyperlink --- .../Pages/GrantApplications/Index.js | 14 +++++++++++++- .../ApplicationBreadcrumbWidget.cs | 1 + .../ApplicationBreadcrumbWidgetViewModel.cs | 1 + .../ApplicationBreadcrumbWidget/Default.cshtml | 14 +++++++++++++- 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Index.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Index.js index 9de9b4752a..dc040ad947 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Index.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Index.js @@ -581,7 +581,19 @@ $(function () { data: 'applicant.applicantName', name: 'applicantName', className: 'data-table-header', - index: columnIndex + index: columnIndex, + render: function(data, type, row) { + let applicantName = (data === "") ? '(Unknown Applicant)' : data; + + const guidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + const isGuid = row.applicant.id && guidPattern.test(row.applicant.id); + + if (isGuid) { + return `${applicantName}`; + } else { + return applicantName; + } + }, } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationBreadcrumbWidget/ApplicationBreadcrumbWidget.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationBreadcrumbWidget/ApplicationBreadcrumbWidget.cs index f229ac79d7..42ae522e40 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationBreadcrumbWidget/ApplicationBreadcrumbWidget.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationBreadcrumbWidget/ApplicationBreadcrumbWidget.cs @@ -26,6 +26,7 @@ public async Task InvokeAsync(Guid applicationId) return View(new ApplicationBreadcrumbWidgetViewModel() { + ApplicantId = applicationApplicant.ApplicantId, ApplicantName = applicationApplicant.ApplicantName, ApplicationStatus = applicationApplicant.ApplicationStatus, ReferenceNo = applicationApplicant.ApplicationReferenceNo, diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationBreadcrumbWidget/ApplicationBreadcrumbWidgetViewModel.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationBreadcrumbWidget/ApplicationBreadcrumbWidgetViewModel.cs index 4ad298762c..5a664ddc66 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationBreadcrumbWidget/ApplicationBreadcrumbWidgetViewModel.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationBreadcrumbWidget/ApplicationBreadcrumbWidgetViewModel.cs @@ -5,6 +5,7 @@ namespace Unity.GrantManager.Web.Views.Shared.Components.ApplicationBreadcrumbWi public class ApplicationBreadcrumbWidgetViewModel { public string ReferenceNo { get; set; } = string.Empty; + public Guid ApplicantId { get; set; } = Guid.Empty; public string ApplicantName { get; set; } = string.Empty; public string ApplicationStatus { get; set; } = string.Empty; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationBreadcrumbWidget/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationBreadcrumbWidget/Default.cshtml index bd60256de7..2eb68d57ef 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationBreadcrumbWidget/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationBreadcrumbWidget/Default.cshtml @@ -6,7 +6,19 @@
@Model.ReferenceNo
-
@Model.ApplicantName
+
+ @if (Model.ApplicantId != Guid.Empty) + { + + @(Model.ApplicantName ?? "(Unknown Applicant)") + + } + else + { + @Model.ApplicantName + } +
@Model.SubmissionFormDescription
@Model.ApplicationStatus
From b37f6ed624c273eb30ee397d6d9675db60c0f55a Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Mon, 9 Mar 2026 12:39:08 -0700 Subject: [PATCH 16/58] feature/AB#31961 - Render Unknown Applicant Name as Hyperlink --- .../Components/ApplicationBreadcrumbWidget/Default.cshtml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationBreadcrumbWidget/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationBreadcrumbWidget/Default.cshtml index 2eb68d57ef..86babd9e71 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationBreadcrumbWidget/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationBreadcrumbWidget/Default.cshtml @@ -2,6 +2,10 @@ @model ApplicationBreadcrumbWidgetViewModel +@{ + var renderedApplicantName = string.IsNullOrWhiteSpace(Model.ApplicantName) ? "Unknown Applicant" : Model.ApplicantName; +} +