diff --git a/.azuredevops/pipelines/gtc-agent-standalone-web-api-sql-aoai.yml b/.azuredevops/pipelines/gtc-agent-standalone-web-api-sql-aoai.yml index dbec37a..aa6cdcb 100644 --- a/.azuredevops/pipelines/gtc-agent-standalone-web-api-sql-aoai.yml +++ b/.azuredevops/pipelines/gtc-agent-standalone-web-api-sql-aoai.yml @@ -103,7 +103,7 @@ variables: value: Release - name: agentProviderKind value: AzureOpenAI - - name: azureOpenAiChatCompletionModelId + - name: azureOpenAiChatDeploymentName value: openai-fast - name: entraExternalIdValidateAuthority value: true @@ -314,7 +314,7 @@ stages: AgentProvider__Kind: $(agentProviderKind) AzureOpenAI__ApiKey: $(azureOpenAiKey) AzureOpenAI__Endpoint: $(azureOpenAiEndpoint) - AzureOpenAI__ChatCompletionModelId: $(azureOpenAiChatCompletionModelId) + AzureOpenAI__ChatDeploymentName: $(azureOpenAiChatDeploymentName) - task: DotNetCoreCLI@2 displayName: Pack Web API artifact @@ -436,7 +436,7 @@ stages: az webapp config appsettings set -g $(azureRgName) -n $(azureApiAppName) --settings AgentProvider:Kind=$(agentProviderKind) az webapp config appsettings set -g $(azureRgName) -n $(azureApiAppName) --settings AzureOpenAI:ApiKey=$(azureOpenAiKey) az webapp config appsettings set -g $(azureRgName) -n $(azureApiAppName) --settings AzureOpenAI:Endpoint=$(azureOpenAiEndpoint) - az webapp config appsettings set -g $(azureRgName) -n $(azureApiAppName) --settings AzureOpenAI:ChatCompletionModelId=$(azureOpenAiChatCompletionModelId) + az webapp config appsettings set -g $(azureRgName) -n $(azureApiAppName) --settings AzureOpenAI:ChatDeploymentName=$(azureOpenAiChatDeploymentName) - task: AzureCLI@2 displayName: $(azureApiAppName) connection strings diff --git a/.github/scripts/ci/Get-CodeCoverage.ps1 b/.github/scripts/ci/Get-CodeCoverage.ps1 index 31214f5..7e083bb 100644 --- a/.github/scripts/ci/Get-CodeCoverage.ps1 +++ b/.github/scripts/ci/Get-CodeCoverage.ps1 @@ -1,24 +1,178 @@ #################################################################################### -# To execute -# 1. In powershell, set security policy for this script: -# Set-ExecutionPolicy Unrestricted -Scope Process -Force -# 2. Change directory to the script folder: -# CD src (wherever your script is) -# 3. In powershell, run script: -# .\Get-CodeCoverage.ps1 -TestProjectFilter '*Tests*.csproj' -# This script uses native .NET 10 code coverage (Microsoft.Testing.Platform) -# Note: Due to MSTest 4.1.0 incompatibility with 'dotnet test' on .NET 10, this runs tests as executables +# Native .NET code coverage summary (plain stats) +# +# Usage: +# .\.github\scripts\ci\Get-CodeCoverage.ps1 +# .\.github\scripts\ci\Get-CodeCoverage.ps1 -TestRootPath .\src +# .\.github\scripts\ci\Get-CodeCoverage.ps1 -IncludeTests +# .\.github\scripts\ci\Get-CodeCoverage.ps1 -TestProjectFilter "*Tests.Integration.csproj" -Configuration Debug #################################################################################### Param( - [string]$TestProjectFilter = '*Tests*.csproj', - [string]$Configuration = 'Release', - [string]$TestRootPath = '' + [string]$TestRootPath = (Join-Path $PSScriptRoot '..\..\..\src'), + [string]$TestProjectFilter = '*Tests*.csproj', + [ValidateSet('Debug', 'Release')] + [string]$Configuration = 'Debug', + [switch]$IncludeTests ) -#################################################################################### -if ($IsWindows) {Set-ExecutionPolicy Unrestricted -Scope Process -Force} -$VerbosePreference = 'SilentlyContinue' # 'Continue' -#################################################################################### + +if ($IsWindows) { + Set-ExecutionPolicy Unrestricted -Scope Process -Force +} + +$ErrorActionPreference = 'Stop' + +function Install-DotnetCoverageTool { + $tool = Get-Command dotnet-coverage -ErrorAction SilentlyContinue + if ($null -eq $tool) { + Write-Host "Installing dotnet-coverage global tool..." + dotnet tool install -g dotnet-coverage | Out-Null + } +} + +function Get-Bar { + param( + [Parameter(Mandatory = $true)] + [double]$Percent, + [int]$Width = 30 + ) + + $clamped = [math]::Max(0, [math]::Min(100, $Percent)) + $filled = [int][math]::Round(($clamped / 100) * $Width) + $empty = $Width - $filled + return ('[' + ('#' * $filled) + ('-' * $empty) + ']') +} + +function Get-CoverageColor { + param( + [Parameter(Mandatory = $true)] + [double]$Percent + ) + + if ($Percent -ge 85) { return 'Green' } + if ($Percent -ge 70) { return 'Yellow' } + return 'Red' +} + +function Get-ModuleDisplayName { + param( + [Parameter(Mandatory = $true)] + [object]$Module + ) + + $name = [string]$Module.module_name + if ([string]::IsNullOrWhiteSpace($name)) { + $name = [string]$Module.name + } + + if ([string]::IsNullOrWhiteSpace($name)) { + $name = [string]$Module.path + } + + if ([string]::IsNullOrWhiteSpace($name)) { + $name = [string]$Module.id + } + + if ([string]::IsNullOrWhiteSpace($name)) { + return '' + } + + return [System.IO.Path]::GetFileNameWithoutExtension($name) +} + +function Test-IsTestArtifact { + param( + [Parameter(Mandatory = $true)] + [string]$Value + ) + + if ([string]::IsNullOrWhiteSpace($Value)) { + return $false + } + + return ($Value -match '(?i)(^|[.\\/_-])tests?([.\\/_-]|$)|integrationtests?') +} + +function Get-FileCoverageFromModules { + param( + [Parameter(Mandatory = $true)] + [object[]]$Modules, + [Parameter(Mandatory = $true)] + [string]$RepoRoot + ) + + $fileStats = @{} + + foreach ($module in $Modules) { + $moduleName = Get-ModuleDisplayName -Module $module + $sourceFileById = @{} + + $sourceFiles = @($module.source_files.source_file) + if ($sourceFiles.Count -eq 0) { + $sourceFiles = @($module.source_file_names.source_file) + } + + foreach ($sourceFile in $sourceFiles) { + $sourceFileById[[string]$sourceFile.id] = [string]$sourceFile.path + } + + foreach ($function in @($module.functions.function)) { + foreach ($range in @($function.ranges.range)) { + $sourceId = [string]$range.source_id + if ([string]::IsNullOrWhiteSpace($sourceId)) { + continue + } + + $sourcePath = $sourceFileById[$sourceId] + if ([string]::IsNullOrWhiteSpace($sourcePath)) { + continue + } + + if (-not $fileStats.ContainsKey($sourcePath)) { + $fileStats[$sourcePath] = @{ + CoveredLines = @{} + TotalLines = @{} + Modules = @{} + } + } + + $startLine = [int]$range.start_line + $endLine = [int]$range.end_line + $isCovered = ([string]$range.covered -eq 'yes') + + for ($line = $startLine; $line -le $endLine; $line++) { + $fileStats[$sourcePath].TotalLines[$line] = $true + if ($isCovered) { + $fileStats[$sourcePath].CoveredLines[$line] = $true + } + } + + $fileStats[$sourcePath].Modules[$moduleName] = $true + } + } + } + + $result = foreach ($path in $fileStats.Keys) { + $coveredCount = $fileStats[$path].CoveredLines.Count + $totalCount = $fileStats[$path].TotalLines.Count + $percent = if ($totalCount -eq 0) { 0 } else { [math]::Round(($coveredCount / [double]$totalCount) * 100, 2) } + + $displayPath = $path + if ($path.StartsWith($RepoRoot, [System.StringComparison]::OrdinalIgnoreCase)) { + $displayPath = $path.Substring($RepoRoot.Length).TrimStart('\') + } + + [PSCustomObject]@{ + Path = $displayPath + Covered = $coveredCount + Total = $totalCount + Percent = $percent + Modules = ($fileStats[$path].Modules.Keys | Sort-Object) -join ', ' + } + } + + return @($result) +} function Resolve-TestRootPath { param( @@ -29,89 +183,188 @@ function Resolve-TestRootPath { ) if (-not [string]::IsNullOrWhiteSpace($OverridePath)) { - return Get-Item -Path (Resolve-Path -Path $OverridePath) + $resolvedOverride = Resolve-Path -Path $OverridePath -ErrorAction SilentlyContinue + if ($null -ne $resolvedOverride) { + return $resolvedOverride.Path + } + + throw "The test root path '$OverridePath' could not be resolved." } $current = $ScriptDir while ($null -ne $current) { $srcCandidate = Join-Path $current.FullName 'src' if (Test-Path -Path $srcCandidate) { - return Get-Item -Path $srcCandidate + return (Resolve-Path -Path $srcCandidate).Path } $current = $current.Parent } - return $ScriptDir + throw "Could not resolve test root path. Pass -TestRootPath explicitly." } -# Install required tools -& dotnet tool install -g dotnet-reportgenerator-globaltool -& dotnet tool install -g dotnet-coverage - -$timestamp = Get-Date -Format "yyyyMMdd-HHmmss" -$scriptPath = Get-Item -Path $PSScriptRoot -$testRootPath = Resolve-TestRootPath -ScriptDir $scriptPath -OverridePath $TestRootPath -$reportOutputPath = Join-Path $testRootPath "TestResults\Reports\$timestamp" +$scriptDir = Get-Item -Path $PSScriptRoot +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..')).Path +$resolvedRoot = Resolve-TestRootPath -ScriptDir $scriptDir -OverridePath $TestRootPath +$resultsRoot = Join-Path $resolvedRoot 'TestResults\Coverage' +New-Item -ItemType Directory -Force -Path $resultsRoot | Out-Null -New-Item -ItemType Directory -Force -Path $reportOutputPath +$runStamp = Get-Date -Format 'yyyyMMdd-HHmmss' +$combinedXml = Join-Path $resultsRoot "coverage-$runStamp.xml" +$runStart = Get-Date -# Find test projects -$testProjects = Get-ChildItem $testRootPath -Filter $TestProjectFilter -Recurse -Write-Host "Found $($testProjects.Count) test projects." +$testProjects = Get-ChildItem -Path $resolvedRoot -Filter $TestProjectFilter -Recurse +if ($testProjects.Count -eq 0) { + Write-Error "No test projects found under '$resolvedRoot' matching '$TestProjectFilter'." +} +Write-Host "Found $($testProjects.Count) test project(s)." +Write-Host '' +Write-Host 'Test Projects' -ForegroundColor Cyan +Write-Host '-------------' -ForegroundColor Cyan foreach ($project in $testProjects) { - $testProjectPath = $project.FullName - Write-Host "Running tests with coverage for project: $($project.BaseName)" + Write-Host "- $($project.Name)" +} +Write-Host '' - Write-Host "Building test project: $($project.BaseName)" - & dotnet build $testProjectPath --configuration $Configuration - - # Use 'dotnet run' instead of 'dotnet test' for MSTest runner projects - # This bypasses the VSTest target that's incompatible with .NET 10 SDK - Push-Location $project.DirectoryName - & dotnet run --configuration $Configuration --no-build -- --coverage - Pop-Location +foreach ($project in $testProjects) { + Write-Host "Running: $($project.FullName)" + dotnet test $project.FullName --configuration $Configuration --collect:"Code Coverage" -v minimal } -# Collect all coverage files (Microsoft.Testing.Platform outputs .coverage files) -$coverageFiles = Get-ChildItem -Path $testRootPath -Filter "*.coverage" -Recurse | Select-Object -ExpandProperty FullName +$coverageFiles = Get-ChildItem -Path $resolvedRoot -Filter '*.coverage' -Recurse | + Where-Object { $_.LastWriteTime -ge $runStart.AddSeconds(-3) } | + Select-Object -ExpandProperty FullName if ($coverageFiles.Count -eq 0) { - Write-Warning "No coverage files found. Make sure your test projects have code coverage enabled." - exit 0 + Write-Error "No .coverage files found after test execution." } -Write-Host "Found $($coverageFiles.Count) coverage file(s)" +Install-DotnetCoverageTool -# Convert binary .coverage files to XML format -$coverageXmlFiles = @() -foreach ($coverageFile in $coverageFiles) { - $xmlFile = $coverageFile -replace '\.coverage$', '.cobertura.xml' - Write-Host "Converting $coverageFile to XML format..." - & dotnet-coverage merge $coverageFile --output $xmlFile --output-format cobertura - if (Test-Path $xmlFile) { - $coverageXmlFiles += $xmlFile +Write-Host "Merging $($coverageFiles.Count) coverage file(s)..." +dotnet-coverage merge $coverageFiles --output $combinedXml --output-format xml | Out-Null + +if (-not (Test-Path -Path $combinedXml)) { + Write-Error "Coverage XML was not generated at '$combinedXml'." +} + +[xml]$xml = Get-Content -Path $combinedXml +$modules = @($xml.results.modules.module) + +if ($modules.Count -eq 0) { + Write-Error "Coverage XML does not contain module data." +} + +$lineCovered = [int](($modules | Measure-Object -Property lines_covered -Sum).Sum) +$lineNotCovered = [int](($modules | Measure-Object -Property lines_not_covered -Sum).Sum) +$linePartial = [int](($modules | Measure-Object -Property lines_partially_covered -Sum).Sum) +$lineTotal = $lineCovered + $lineNotCovered + $linePartial +$linePct = if ($lineTotal -eq 0) { 0 } else { [math]::Round((($lineCovered + $linePartial) / [double]$lineTotal) * 100, 2) } + +$blockCovered = [int](($modules | Measure-Object -Property blocks_covered -Sum).Sum) +$blockNotCovered = [int](($modules | Measure-Object -Property blocks_not_covered -Sum).Sum) +$blockTotal = $blockCovered + $blockNotCovered +$blockPct = if ($blockTotal -eq 0) { 0 } else { [math]::Round(($blockCovered / [double]$blockTotal) * 100, 2) } + +$moduleCoverage = foreach ($module in $modules) { + $moduleLineCovered = [int]$module.lines_covered + $moduleLineNotCovered = [int]$module.lines_not_covered + $moduleLinePartial = [int]$module.lines_partially_covered + $moduleLineTotal = $moduleLineCovered + $moduleLineNotCovered + $moduleLinePartial + $moduleLinePct = if ($moduleLineTotal -eq 0) { 0 } else { [math]::Round((($moduleLineCovered + $moduleLinePartial) / [double]$moduleLineTotal) * 100, 2) } + + [PSCustomObject]@{ + Name = Get-ModuleDisplayName -Module $module + Covered = $moduleLineCovered + $moduleLinePartial + Total = $moduleLineTotal + Percent = $moduleLinePct } } -if ($coverageXmlFiles.Count -eq 0) { - Write-Warning "No XML coverage files were generated." - exit 0 +$moduleCoverageSorted = $moduleCoverage | Sort-Object -Property Percent, Name + +$fileCoverage = Get-FileCoverageFromModules -Modules $modules -RepoRoot $repoRoot + +$moduleCoverageDisplay = if ($IncludeTests) { + $moduleCoverage +} else { + $moduleCoverage | Where-Object { -not (Test-IsTestArtifact -Value $_.Name) } } -Write-Host "Generated $($coverageXmlFiles.Count) XML coverage file(s)" +$fileCoverageDisplay = if ($IncludeTests) { + $fileCoverage +} else { + $fileCoverage | Where-Object { + (-not (Test-IsTestArtifact -Value $_.Path)) -and + (-not (Test-IsTestArtifact -Value $_.Modules)) + } +} -# Generate HTML report -$coverageFilesArg = ($coverageXmlFiles -join ";") -& reportgenerator -reports:$coverageFilesArg -targetdir:$reportOutputPath -reporttypes:Html +$moduleCoverageSorted = $moduleCoverageDisplay | Sort-Object -Property Percent, Name +$moduleCoverageTop = $moduleCoverageSorted | Select-Object -Last 5 +$moduleCoverageBottom = $moduleCoverageSorted | Select-Object -First 5 -Write-Host "Code coverage report generated at: $reportOutputPath" +$fileCoverageSorted = $fileCoverageDisplay | Sort-Object -Property Percent, Path +$fileCoverageBottom = $fileCoverageSorted | Select-Object -First 10 -$reportIndexHtml = Join-Path $reportOutputPath "index.html" -if (Test-Path $reportIndexHtml) { - Invoke-Item -Path $reportIndexHtml +Write-Host '' +Write-Host 'Coverage Summary' -ForegroundColor Cyan +Write-Host '----------------' -ForegroundColor Cyan +if ($IncludeTests) { + Write-Host 'Scope : Including test projects and files' -ForegroundColor DarkGray +} else { + Write-Host 'Scope : Excluding test projects and files (use -IncludeTests to include)' -ForegroundColor DarkGray } -else { - Write-Warning "Report index.html not found at: $reportIndexHtml" -} \ No newline at end of file + +$lineBar = Get-Bar -Percent $linePct +$lineColor = Get-CoverageColor -Percent $linePct +Write-Host ("Line coverage : {0} {1}% ({2}/{3})" -f $lineBar, $linePct, $lineCovered, $lineTotal) -ForegroundColor $lineColor + +$blockBar = Get-Bar -Percent $blockPct +$blockColor = Get-CoverageColor -Percent $blockPct +Write-Host ("Block coverage: {0} {1}% ({2}/{3})" -f $blockBar, $blockPct, $blockCovered, $blockTotal) -ForegroundColor $blockColor + +Write-Host '' +Write-Host 'Lowest Coverage Projects/Assemblies' -ForegroundColor Yellow +Write-Host '-----------------------------------' -ForegroundColor Yellow +if ($moduleCoverageBottom.Count -gt 0) { + foreach ($item in $moduleCoverageBottom) { + $itemBar = Get-Bar -Percent $item.Percent -Width 20 + $itemColor = Get-CoverageColor -Percent $item.Percent + Write-Host ("{0,-40} {1} {2,6}% ({3}/{4})" -f $item.Name, $itemBar, $item.Percent, $item.Covered, $item.Total) -ForegroundColor $itemColor + } +} else { + Write-Host 'No projects/assemblies available after filtering.' -ForegroundColor Yellow +} + +Write-Host '' +Write-Host 'Highest Coverage Projects/Assemblies' -ForegroundColor Green +Write-Host '------------------------------------' -ForegroundColor Green +if ($moduleCoverageTop.Count -gt 0) { + foreach ($item in ($moduleCoverageTop | Sort-Object -Property Percent, Name -Descending)) { + $itemBar = Get-Bar -Percent $item.Percent -Width 20 + $itemColor = Get-CoverageColor -Percent $item.Percent + Write-Host ("{0,-40} {1} {2,6}% ({3}/{4})" -f $item.Name, $itemBar, $item.Percent, $item.Covered, $item.Total) -ForegroundColor $itemColor + } +} else { + Write-Host 'No projects/assemblies available after filtering.' -ForegroundColor Yellow +} + +if ($fileCoverageBottom.Count -gt 0) { + Write-Host '' + Write-Host 'Lowest Coverage Files (Top 10 To Improve)' -ForegroundColor Magenta + Write-Host '-----------------------------------------' -ForegroundColor Magenta + foreach ($item in $fileCoverageBottom) { + $itemBar = Get-Bar -Percent $item.Percent -Width 16 + $itemColor = Get-CoverageColor -Percent $item.Percent + Write-Host ("{0,-60} {1} {2,6}% ({3}/{4})" -f $item.Path, $itemBar, $item.Percent, $item.Covered, $item.Total) -ForegroundColor $itemColor + } +} else { + Write-Host '' + Write-Host 'File-level coverage details were not available in this XML format.' -ForegroundColor Yellow +} + +Write-Host '' +Write-Host "Coverage xml : $combinedXml" -ForegroundColor DarkGray diff --git a/README.md b/README.md index 759fc84..dd5d58a 100644 --- a/README.md +++ b/README.md @@ -52,14 +52,14 @@ cd src/Presentation.Api dotnet user-secrets set "AgentProvider:Kind" "AzureOpenAI" # Set Azure OpenAI settings in API project -dotnet user-secrets set "AzureOpenAI:ChatCompletionModelId" "gpt-4" +dotnet user-secrets set "AzureOpenAI:ChatDeploymentName" "gpt-4" dotnet user-secrets set "AzureOpenAI:Endpoint" "https://YOUR_ENDPOINT.openai.azure.com/" dotnet user-secrets set "AzureOpenAI:ApiKey" "YOUR_API_KEY" # Set Azure OpenAI settings in integration test project cd ../Tests.Integration dotnet user-secrets init -dotnet user-secrets set "AzureOpenAI:ChatCompletionModelId" "gpt-4" +dotnet user-secrets set "AzureOpenAI:ChatDeploymentName" "gpt-4" dotnet user-secrets set "AzureOpenAI:Endpoint" "https://YOUR_ENDPOINT.openai.azure.com/" dotnet user-secrets set "AzureOpenAI:ApiKey" "YOUR_API_KEY" @@ -95,7 +95,7 @@ dotnet user-secrets set "EntraExternalId:ClientId" "API_CLIENT_ID" dotnet user-secrets set "EntraExternalId:ValidateAuthority" "true" dotnet user-secrets set "ApplicationInsights:ConnectionString" "AZURE_MONITOR_CONNECTION_STRING" dotnet user-secrets set "AgentProvider:Kind" "AzureOpenAI" -dotnet user-secrets set "AzureOpenAI:ChatCompletionModelId" "gpt-4" +dotnet user-secrets set "AzureOpenAI:ChatDeploymentName" "gpt-4" dotnet user-secrets set "AzureOpenAI:Endpoint" "https://YOUR_ENDPOINT.openai.azure.com/" dotnet user-secrets set "AzureOpenAI:ApiKey" "YOUR_API_KEY" @@ -114,7 +114,7 @@ dotnet user-secrets set "ApplicationInsights:ConnectionString" "AZURE_MONITOR_CO # Set integration test Azure OpenAI settings cd ../Tests.Integration dotnet user-secrets init -dotnet user-secrets set "AzureOpenAI:ChatCompletionModelId" "gpt-4" +dotnet user-secrets set "AzureOpenAI:ChatDeploymentName" "gpt-4" dotnet user-secrets set "AzureOpenAI:Endpoint" "https://YOUR_ENDPOINT.openai.azure.com/" dotnet user-secrets set "AzureOpenAI:ApiKey" "YOUR_API_KEY" @@ -276,18 +276,18 @@ Set `ASPNETCORE_ENVIRONMENT` manually only when running outside launch profiles ``` cd src/Presentation.Api dotnet user-secrets set "AgentProvider:Kind" "AzureOpenAI" -dotnet user-secrets set "AzureOpenAI:ChatCompletionModelId" "gpt-4" +dotnet user-secrets set "AzureOpenAI:ChatDeploymentName" "gpt-4" dotnet user-secrets set "AzureOpenAI:Endpoint" "https://YOUR_ENDPOINT.openai.azure.com/" dotnet user-secrets set "AzureOpenAI:ApiKey" "YOUR_API_KEY" cd ../Tests.Integration dotnet user-secrets init -dotnet user-secrets set "AzureOpenAI:ChatCompletionModelId" "gpt-4" +dotnet user-secrets set "AzureOpenAI:ChatDeploymentName" "gpt-4" dotnet user-secrets set "AzureOpenAI:Endpoint" "https://YOUR_ENDPOINT.openai.azure.com/" dotnet user-secrets set "AzureOpenAI:ApiKey" "YOUR_API_KEY" ``` Alternately you can set in Environment variables ``` -AzureOpenAI__ChatCompletionModelId +AzureOpenAI__ChatDeploymentName AzureOpenAI__Endpoint AzureOpenAI__ApiKey ``` @@ -463,5 +463,6 @@ This setup ensures your GitHub Actions workflows can securely deploy to Azure us | Version | Date | Release Notes | |---------|-------------|------------------------------------------------------------------| | 1.0.0 | 2026-Feb-02 | Initial Release | +| 1.1.0 | 2026-Jul-27 | AI-ize md/yml, AgentProvider | This project is licensed with the [MIT license](https://mit-license.org/). \ No newline at end of file diff --git a/src/Core.Application/Core.Application.csproj b/src/Core.Application/Core.Application.csproj index f26c0fc..0adeb4f 100644 --- a/src/Core.Application/Core.Application.csproj +++ b/src/Core.Application/Core.Application.csproj @@ -23,8 +23,8 @@ - - + + diff --git a/src/Infrastructure.AgentFramework/AiModels/HuggingfaceModels.cs b/src/Infrastructure.AgentFramework/AiModels/HuggingfaceModels.cs deleted file mode 100644 index eb8d84c..0000000 --- a/src/Infrastructure.AgentFramework/AiModels/HuggingfaceModels.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Goodtocode.AgentFramework.Infrastructure.AgentFramework.AiModels; - -public struct HuggingfaceModels -{ - public const string BertBaseUncased = "bert-base-uncased"; - public const string ChatGpt2 = "gpt2"; - public const string RobertaBase = "roberta-base"; - public const string NokiaNlgpNatural = "nokia/nlgp-natural"; -} diff --git a/src/Infrastructure.AgentFramework/AiModels/OpenAiModels.cs b/src/Infrastructure.AgentFramework/AiModels/OpenAiModels.cs deleted file mode 100644 index fa90573..0000000 --- a/src/Infrastructure.AgentFramework/AiModels/OpenAiModels.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace Goodtocode.AgentFramework.Infrastructure.AgentFramework.AiModels; - -public struct OpenAiModels -{ - public struct ChatCompletion - { - public const string ChatGpt4 = "gpt-4"; - public const string ChatGpt4Turbo = "gpt-4-turbo"; - public const string ChatGpt35Turbo = "gpt-3.5-turbo"; - } - public struct TextGeneration - { - public const string ChatGpt35TurboInstruct = "gpt-3.5-turbo-instruct"; - } - public struct TextEmbedding - { - public const string TextEmbedding3Large = "text-embedding-3-large"; - public const string TextEmbedding3Small = "text-embedding-3-small"; - } - public struct TextModeration - { - public const string TextModerationLatest = "text-moderation-latest"; - } - public struct Image - { - public const string Dalle3 = "dall-e-3"; - public const string Dalle2 = "dall-e-2"; - } - public struct Audio - { - public const string TextToSpeech1 = "tts-1"; - public const string Whisper1 = "whisper-1"; - public const string Whisper2 = "whisper-2"; - } -} diff --git a/src/Infrastructure.AgentFramework/ConfigureServices.cs b/src/Infrastructure.AgentFramework/ConfigureServices.cs index 77a5f41..2c37a7c 100644 --- a/src/Infrastructure.AgentFramework/ConfigureServices.cs +++ b/src/Infrastructure.AgentFramework/ConfigureServices.cs @@ -95,11 +95,11 @@ public static IServiceCollection AddAgentFrameworkOpenAIServices(this IServiceCo { var options = provider.GetRequiredService>().Value; var normalizedEndpoint = NormalizeAzureOpenAIEndpoint(options.Endpoint); - logger.LogInformation("Agent provider resolved to {Provider}. Endpoint: {Endpoint}. NormalizedEndpoint: {NormalizedEndpoint}. ChatCompletionModelId: {ChatCompletionModelId}", + logger.LogInformation("Agent provider resolved to {Provider}. Endpoint: {Endpoint}. NormalizedEndpoint: {NormalizedEndpoint}. ChatDeploymentName: {ChatDeploymentName}", providerOptions.Kind, options.Endpoint, normalizedEndpoint, - options.ChatCompletionModelId); + options.ChatDeploymentName); var client = new OpenAIClient( new System.ClientModel.ApiKeyCredential(options.ApiKey), @@ -107,7 +107,7 @@ public static IServiceCollection AddAgentFrameworkOpenAIServices(this IServiceCo { Endpoint = new Uri(normalizedEndpoint) }); - return client.GetChatClient(options.ChatCompletionModelId); + return client.GetChatClient(options.ChatDeploymentName); } if (providerOptions.Kind.Equals("Ollama", StringComparison.OrdinalIgnoreCase)) @@ -246,7 +246,7 @@ private static bool HasRequiredGitHubCopilotSdk(GitHubCopilotSdkOptions options) && HasUri(options.Endpoint); private static bool HasRequiredAzureOpenAI(AzureOpenAIOptions options) - => HasValue(options.ChatCompletionModelId) + => HasValue(options.ChatDeploymentName) && HasValue(options.ApiKey) && HasUri(options.Endpoint); diff --git a/src/Infrastructure.AgentFramework/Infrastructure.AgentFramework.csproj b/src/Infrastructure.AgentFramework/Infrastructure.AgentFramework.csproj index c53e8b1..43bf765 100644 --- a/src/Infrastructure.AgentFramework/Infrastructure.AgentFramework.csproj +++ b/src/Infrastructure.AgentFramework/Infrastructure.AgentFramework.csproj @@ -12,14 +12,14 @@ - - - - - - - - + + + + + + + + diff --git a/src/Infrastructure.AgentFramework/Options/AgentProviderOptions.cs b/src/Infrastructure.AgentFramework/Options/AgentProviderOptions.cs index 2cb8943..09966bc 100644 --- a/src/Infrastructure.AgentFramework/Options/AgentProviderOptions.cs +++ b/src/Infrastructure.AgentFramework/Options/AgentProviderOptions.cs @@ -8,5 +8,5 @@ public sealed class AgentProviderOptions [Required] [RegularExpression("^(OpenAI|GitHubCopilotSDK|AzureOpenAI|AzureAIFoundry|Ollama)$")] - public string Kind { get; set; } = "OpenAI"; + public string Kind { get; set; } = "Ollama"; } diff --git a/src/Infrastructure.AgentFramework/Options/AzureOpenAIOptions.cs b/src/Infrastructure.AgentFramework/Options/AzureOpenAIOptions.cs index 8bc1be4..f9eb541 100644 --- a/src/Infrastructure.AgentFramework/Options/AzureOpenAIOptions.cs +++ b/src/Infrastructure.AgentFramework/Options/AzureOpenAIOptions.cs @@ -10,7 +10,7 @@ public sealed class AzureOpenAIOptions public const string SectionName = "AzureOpenAI"; [Required] - public string ChatCompletionModelId { get; set; } = string.Empty; + public string ChatDeploymentName { get; set; } = string.Empty; [Required] public string Endpoint { get; set; } = string.Empty; diff --git a/src/Infrastructure.SqlServer/Infrastructure.SqlServer.csproj b/src/Infrastructure.SqlServer/Infrastructure.SqlServer.csproj index dafda06..35bb01c 100644 --- a/src/Infrastructure.SqlServer/Infrastructure.SqlServer.csproj +++ b/src/Infrastructure.SqlServer/Infrastructure.SqlServer.csproj @@ -10,16 +10,16 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - + + + + + diff --git a/src/Presentation.Api/Identity/Auth/HttpClaimsReader.cs b/src/Presentation.Api/Common/Auth/HttpClaimsReader.cs similarity index 98% rename from src/Presentation.Api/Identity/Auth/HttpClaimsReader.cs rename to src/Presentation.Api/Common/Auth/HttpClaimsReader.cs index ab67863..35c882d 100644 --- a/src/Presentation.Api/Identity/Auth/HttpClaimsReader.cs +++ b/src/Presentation.Api/Common/Auth/HttpClaimsReader.cs @@ -1,5 +1,4 @@ - -namespace Goodtocode.AgentFramework.Presentation.Api.Identity.Auth; +namespace Goodtocode.AgentFramework.Presentation.Api.Common.Auth; /// /// Infrastructure implementation that reads user claims from the current HTTP request context. diff --git a/src/Presentation.Api/Identity/Auth/InjectUserContextBehavior.cs b/src/Presentation.Api/Common/Auth/InjectUserContextBehavior.cs similarity index 97% rename from src/Presentation.Api/Identity/Auth/InjectUserContextBehavior.cs rename to src/Presentation.Api/Common/Auth/InjectUserContextBehavior.cs index 370db03..77d6a0c 100644 --- a/src/Presentation.Api/Identity/Auth/InjectUserContextBehavior.cs +++ b/src/Presentation.Api/Common/Auth/InjectUserContextBehavior.cs @@ -1,4 +1,4 @@ -namespace Goodtocode.AgentFramework.Presentation.Api.Identity.Auth; +namespace Goodtocode.AgentFramework.Presentation.Api.Common.Auth; /// /// Pipeline behavior that automatically injects authenticated user context into requests diff --git a/src/Presentation.Api/Common/ApiExceptionHandler.cs b/src/Presentation.Api/Common/Exceptions/ApiExceptionHandler.cs similarity index 98% rename from src/Presentation.Api/Common/ApiExceptionHandler.cs rename to src/Presentation.Api/Common/Exceptions/ApiExceptionHandler.cs index 98b67c5..c2924f3 100644 --- a/src/Presentation.Api/Common/ApiExceptionHandler.cs +++ b/src/Presentation.Api/Common/Exceptions/ApiExceptionHandler.cs @@ -1,4 +1,4 @@ -namespace Goodtocode.AgentFramework.Presentation.Api.Common; +namespace Goodtocode.AgentFramework.Presentation.Api.Common.Exceptions; /// /// Handles unhandled API exceptions and maps them to HTTP problem responses. diff --git a/src/Presentation.Api/Common/ApiExceptionHandlingExtensions.cs b/src/Presentation.Api/Common/Exceptions/ApiExceptionHandlingExtensions.cs similarity index 89% rename from src/Presentation.Api/Common/ApiExceptionHandlingExtensions.cs rename to src/Presentation.Api/Common/Exceptions/ApiExceptionHandlingExtensions.cs index 07e8d50..cf4104f 100644 --- a/src/Presentation.Api/Common/ApiExceptionHandlingExtensions.cs +++ b/src/Presentation.Api/Common/Exceptions/ApiExceptionHandlingExtensions.cs @@ -1,4 +1,4 @@ -namespace Goodtocode.AgentFramework.Presentation.Api.Common; +namespace Goodtocode.AgentFramework.Presentation.Api.Common.Exceptions; /// /// Provides service registration helpers for API exception handling. diff --git a/src/Presentation.Api/Identity/Auth/ConfigureServices.cs b/src/Presentation.Api/ConfigureServicesAuth.cs similarity index 90% rename from src/Presentation.Api/Identity/Auth/ConfigureServices.cs rename to src/Presentation.Api/ConfigureServicesAuth.cs index 6463b42..97ced02 100644 --- a/src/Presentation.Api/Identity/Auth/ConfigureServices.cs +++ b/src/Presentation.Api/ConfigureServicesAuth.cs @@ -1,12 +1,14 @@ -using Microsoft.AspNetCore.Authentication.JwtBearer; +using Goodtocode.AgentFramework.Presentation.Api.Common.Auth; +using Goodtocode.AgentFramework.Presentation.Api.Options; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Identity.Web; -namespace Goodtocode.AgentFramework.Presentation.Api.Identity.Auth; +namespace Goodtocode.AgentFramework.Presentation.Api; /// /// Presentation Layer WebApi Authentication Configuration /// -public static class ConfigureServices +public static class ConfigureServicesAuth { private struct TokenRoleClaimTypes { diff --git a/src/Presentation.Api/Options/ConductorApiOptions.cs b/src/Presentation.Api/Options/ConductorApiOptions.cs deleted file mode 100644 index c8bbb28..0000000 --- a/src/Presentation.Api/Options/ConductorApiOptions.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Goodtocode.AgentFramework.Presentation.Api.Options; - -/// -/// Conductor capability API settings. -/// -public sealed class ConductorApiOptions -{ - /// - /// Configuration section name for Conductor capability API settings. - /// - public const string SectionName = "ConductorApi"; - - /// - /// Base URL of the Conductor capability API. - /// - [Required] - public Uri BaseUrl { get; set; } = default!; -} diff --git a/src/Presentation.Api/Identity/Auth/EntraExternalIdApiOptions.cs b/src/Presentation.Api/Options/EntraExternalIdApiOptions.cs similarity index 94% rename from src/Presentation.Api/Identity/Auth/EntraExternalIdApiOptions.cs rename to src/Presentation.Api/Options/EntraExternalIdApiOptions.cs index 473e562..67455bd 100644 --- a/src/Presentation.Api/Identity/Auth/EntraExternalIdApiOptions.cs +++ b/src/Presentation.Api/Options/EntraExternalIdApiOptions.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace Goodtocode.AgentFramework.Presentation.Api.Identity.Auth; +namespace Goodtocode.AgentFramework.Presentation.Api.Options; /// /// Configuration values for API-side Entra External ID token validation. diff --git a/src/Presentation.Api/Presentation.Api.csproj b/src/Presentation.Api/Presentation.Api.csproj index b4f5603..58c81b1 100644 --- a/src/Presentation.Api/Presentation.Api.csproj +++ b/src/Presentation.Api/Presentation.Api.csproj @@ -16,23 +16,23 @@ - + - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + diff --git a/src/Presentation.Api/Program.cs b/src/Presentation.Api/Program.cs index 90dfd6e..1a09e47 100644 --- a/src/Presentation.Api/Program.cs +++ b/src/Presentation.Api/Program.cs @@ -4,8 +4,8 @@ using Goodtocode.AgentFramework.Infrastructure.AgentFramework; using Goodtocode.AgentFramework.Infrastructure.SqlServer; using Goodtocode.AgentFramework.Presentation.Api; +using Goodtocode.AgentFramework.Presentation.Api.Common.Exceptions; using Goodtocode.AgentFramework.Presentation.Api.Endpoints; -using Goodtocode.AgentFramework.Presentation.Api.Identity.Auth; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; diff --git a/src/Presentation.Api/ProgramMarker.cs b/src/Presentation.Api/ProgramMarker.cs deleted file mode 100644 index 54184e1..0000000 --- a/src/Presentation.Api/ProgramMarker.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Goodtocode.AgentFramework.Presentation.Api; - -/// -/// Program marker type for integration scenarios and tooling. -/// -public partial class Program; diff --git a/src/Presentation.Api/appsettings.Development.json b/src/Presentation.Api/appsettings.Development.json index 449b673..4aa99cf 100644 --- a/src/Presentation.Api/appsettings.Development.json +++ b/src/Presentation.Api/appsettings.Development.json @@ -33,7 +33,7 @@ "Endpoint": "http://localhost:11434/api" }, "AzureOpenAI": { - "ChatCompletionModelId": "openai-fast", // aka. gpt-5.4-mini + "ChatDeploymentName": "openai-fast", // aka. gpt-5.4-mini "Endpoint": "https://your-resource.openai.azure.com", "ApiKey": "" }, diff --git a/src/Presentation.Api/appsettings.Production.json b/src/Presentation.Api/appsettings.Production.json index ae68b21..15f8910 100644 --- a/src/Presentation.Api/appsettings.Production.json +++ b/src/Presentation.Api/appsettings.Production.json @@ -33,7 +33,7 @@ "Endpoint": "http://localhost:11434/api" }, "AzureOpenAI": { - "ChatCompletionModelId": "openai-chat", // aka. gpt-5.4 + "ChatDeploymentName": "openai-chat", // aka. gpt-5.4 "Endpoint": "https://your-resource.openai.azure.com", "ApiKey": "" }, diff --git a/src/Presentation.Api/appsettings.local.json b/src/Presentation.Api/appsettings.local.json index 3623acc..24330fa 100644 --- a/src/Presentation.Api/appsettings.local.json +++ b/src/Presentation.Api/appsettings.local.json @@ -33,7 +33,7 @@ "Endpoint": "http://localhost:11434/api" }, "AzureOpenAI": { - "ChatCompletionModelId": "openai-fast", // aka. gpt-5.4-mini + "ChatDeploymentName": "openai-fast", // aka. gpt-5.4-mini "Endpoint": "https://your-resource.openai.azure.com/openai/v1", "ApiKey": "" }, diff --git a/src/Presentation.Web/ConfigureServices.cs b/src/Presentation.Web/ConfigureServices.cs index 6276e3d..e9ced14 100644 --- a/src/Presentation.Web/ConfigureServices.cs +++ b/src/Presentation.Web/ConfigureServices.cs @@ -6,7 +6,7 @@ using Microsoft.FluentUI.AspNetCore.Components; using Goodtocode.AgentFramework.Presentation.Web.Library.Auth.Services; using Goodtocode.AgentFramework.Presentation.Web.Library.Auth; -using Goodtocode.AgentFramework.Presentation.Web.Infrastructure.Options; +using Goodtocode.AgentFramework.Presentation.Web.Options; namespace Goodtocode.AgentFramework.Presentation.Web; diff --git a/src/Presentation.Web/ConfigureServicesAuth.cs b/src/Presentation.Web/ConfigureServicesAuth.cs index 3b963c1..300a086 100644 --- a/src/Presentation.Web/ConfigureServicesAuth.cs +++ b/src/Presentation.Web/ConfigureServicesAuth.cs @@ -1,6 +1,6 @@ -using Goodtocode.AgentFramework.Presentation.Web.Infrastructure.Options; -using Goodtocode.AgentFramework.Presentation.Web.Library.Auth.Middleware; +using Goodtocode.AgentFramework.Presentation.Web.Library.Auth.Middleware; using Goodtocode.AgentFramework.Presentation.Web.Library.Auth.Services; +using Goodtocode.AgentFramework.Presentation.Web.Options; using Goodtocode.SecuredHttpClient.Middleware; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.Identity.Web; diff --git a/src/Presentation.Web/Library/Auth/Middleware/DownstreamApiAccessTokenProvider.cs b/src/Presentation.Web/Library/Auth/Middleware/DownstreamApiAccessTokenProvider.cs index f17b5ef..f8e0410 100644 --- a/src/Presentation.Web/Library/Auth/Middleware/DownstreamApiAccessTokenProvider.cs +++ b/src/Presentation.Web/Library/Auth/Middleware/DownstreamApiAccessTokenProvider.cs @@ -1,7 +1,7 @@ using Goodtocode.SecuredHttpClient.Middleware; using Microsoft.Identity.Web; using Microsoft.Extensions.Options; -using Goodtocode.AgentFramework.Presentation.Web.Infrastructure.Options; +using Goodtocode.AgentFramework.Presentation.Web.Options; namespace Goodtocode.AgentFramework.Presentation.Web.Library.Auth.Middleware; diff --git a/src/Presentation.Web/Library/Auth/Routing/RedirectToResetPassword.razor b/src/Presentation.Web/Library/Auth/Routing/RedirectToResetPassword.razor index b826c0c..4c1fc28 100644 --- a/src/Presentation.Web/Library/Auth/Routing/RedirectToResetPassword.razor +++ b/src/Presentation.Web/Library/Auth/Routing/RedirectToResetPassword.razor @@ -1,5 +1,5 @@ @inject NavigationManager Navigation -@inject Microsoft.Extensions.Options.IOptions EntraExternalIdOptions +@inject Microsoft.Extensions.Options.IOptions EntraExternalIdOptions Redirecting to password reset... diff --git a/src/Presentation.Web/Infrastructure/Options/BackendApiOptions.cs b/src/Presentation.Web/Options/BackendApiOptions.cs similarity index 81% rename from src/Presentation.Web/Infrastructure/Options/BackendApiOptions.cs rename to src/Presentation.Web/Options/BackendApiOptions.cs index 5c4f9fe..1e84c0c 100644 --- a/src/Presentation.Web/Infrastructure/Options/BackendApiOptions.cs +++ b/src/Presentation.Web/Options/BackendApiOptions.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace Goodtocode.AgentFramework.Presentation.Web.Infrastructure.Options; +namespace Goodtocode.AgentFramework.Presentation.Web.Options; /// /// Presentation.Api settings diff --git a/src/Presentation.Web/Infrastructure/Options/EntraExternalIdWebOptions.cs b/src/Presentation.Web/Options/EntraExternalIdWebOptions.cs similarity index 95% rename from src/Presentation.Web/Infrastructure/Options/EntraExternalIdWebOptions.cs rename to src/Presentation.Web/Options/EntraExternalIdWebOptions.cs index 2dd8480..df6f5b9 100644 --- a/src/Presentation.Web/Infrastructure/Options/EntraExternalIdWebOptions.cs +++ b/src/Presentation.Web/Options/EntraExternalIdWebOptions.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace Goodtocode.AgentFramework.Presentation.Web.Infrastructure.Options; +namespace Goodtocode.AgentFramework.Presentation.Web.Options; /// /// Configuration values for Web app Entra External ID sign-in and downstream API access. diff --git a/src/Presentation.Web/Infrastructure/Options/ResilientHttpClientOptions.cs b/src/Presentation.Web/Options/ResilientHttpClientOptions.cs similarity index 69% rename from src/Presentation.Web/Infrastructure/Options/ResilientHttpClientOptions.cs rename to src/Presentation.Web/Options/ResilientHttpClientOptions.cs index e3db7a9..dc7afc2 100644 --- a/src/Presentation.Web/Infrastructure/Options/ResilientHttpClientOptions.cs +++ b/src/Presentation.Web/Options/ResilientHttpClientOptions.cs @@ -1,4 +1,4 @@ -namespace Goodtocode.AgentFramework.Presentation.Web.Infrastructure.Options; +namespace Goodtocode.AgentFramework.Presentation.Web.Options; public class ResilientHttpClientOptions { diff --git a/src/Presentation.Web/Presentation.Web.csproj b/src/Presentation.Web/Presentation.Web.csproj index 10d3303..a27377b 100644 --- a/src/Presentation.Web/Presentation.Web.csproj +++ b/src/Presentation.Web/Presentation.Web.csproj @@ -13,16 +13,16 @@ - + - - + + - - - + + + diff --git a/src/Tests.Integration/Tests.Integration.csproj b/src/Tests.Integration/Tests.Integration.csproj index 7b5aa69..73ccb4f 100644 --- a/src/Tests.Integration/Tests.Integration.csproj +++ b/src/Tests.Integration/Tests.Integration.csproj @@ -20,10 +20,10 @@ - - - - + + + +