diff --git a/.github/workflows/e2e-dotnet-agent-framework.yml b/.github/workflows/e2e-dotnet-agent-framework.yml index 9a7c3349..e7413971 100644 --- a/.github/workflows/e2e-dotnet-agent-framework.yml +++ b/.github/workflows/e2e-dotnet-agent-framework.yml @@ -56,114 +56,4 @@ jobs: -Runtime "dotnet" ` -WorkingDirectory "${{ env.SAMPLE_PATH }}" - - name: Acquire Bearer Token (ROPC) - id: token - shell: pwsh - run: | - $token = & "${{ env.SCRIPTS_PATH }}/Acquire-BearerToken.ps1" ` - -ClientId "${{ secrets.MCP_CLIENT_ID }}" ` - -TenantId "${{ secrets.MCP_TENANT_ID }}" ` - -Username "${{ secrets.MCP_TEST_USERNAME }}" ` - -Password "${{ secrets.MCP_TEST_PASSWORD }}" - echo "BEARER_TOKEN=$token" >> $env:GITHUB_OUTPUT - echo "::add-mask::$token" - - - name: Copy ToolingManifest.json - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Copy-ToolingManifest.ps1" -TargetPath "${{ env.SAMPLE_PATH }}" - - - name: Generate appsettings.json - shell: pwsh - run: | - $configMappings = @{ - "AIServices:AzureOpenAI:Endpoint" = "${{ secrets.DOTNET_AF_AZURE_OPENAI_ENDPOINT }}" - "AIServices:AzureOpenAI:ApiKey" = "${{ secrets.DOTNET_AF_AZURE_OPENAI_API_KEY }}" - "AIServices:AzureOpenAI:DeploymentName" = "${{ secrets.DOTNET_AF_AZURE_OPENAI_DEPLOYMENT }}" - "TokenValidation:Enabled" = "false" - "Connections:ServiceConnection:Settings:AuthType" = "ClientSecret" - "Connections:ServiceConnection:Settings:ClientId" = "${{ secrets.DOTNET_AF_CLIENT_ID }}" - "Connections:ServiceConnection:Settings:ClientSecret" = "${{ secrets.DOTNET_AF_CLIENT_SECRET }}" - "Connections:ServiceConnection:Settings:TenantId" = "${{ secrets.TENANT_ID }}" - "Connections:ServiceConnection:Settings:AuthorityEndpoint" = "https://login.microsoftonline.com/${{ secrets.TENANT_ID }}" - "Connections:ServiceConnection:Settings:Scopes:0" = "5a807f24-c9de-44ee-a3a7-329e88a00ffc/.default" - } - & "${{ env.SCRIPTS_PATH }}/Generate-AppSettings.ps1" ` - -OutputPath "${{ env.SAMPLE_PATH }}/appsettings.json" ` - -ConfigMappings $configMappings - - - name: Start Agent - id: start-agent - shell: pwsh - run: | - $agentPid = & "${{ env.SCRIPTS_PATH }}/Start-Agent.ps1" ` - -AgentPath "${{ env.SAMPLE_PATH }}" ` - -StartCommand "dotnet run --no-build" ` - -Port ${{ env.AGENT_PORT }} ` - -BearerToken "${{ steps.token.outputs.BEARER_TOKEN }}" ` - -Environment "Development" ` - -Runtime "dotnet" - echo "AGENT_PID=$agentPid" >> $env:GITHUB_OUTPUT - - - name: Verify Agent Running - shell: pwsh - run: | - $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" - if ($agentPid) { - try { - $proc = Get-Process -Id $agentPid -ErrorAction Stop - Write-Host "Agent process (PID: $agentPid) is running: $($proc.ProcessName)" -ForegroundColor Green - } catch { - Write-Host "ERROR: Agent process (PID: $agentPid) is NOT running!" -ForegroundColor Red - throw "Agent process has stopped" - } - } - $response = Invoke-WebRequest -Uri "http://localhost:${{ env.AGENT_PORT }}/api/health" -UseBasicParsing -ErrorAction SilentlyContinue - Write-Host "Health: $($response.StatusCode)" -ForegroundColor Green - - name: Restore E2E Test Dependencies - run: dotnet restore "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" - - - name: Run .NET E2E Tests - shell: pwsh - run: | - dotnet test "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" ` - --no-restore ` - --verbosity normal ` - --logger "console;verbosity=detailed" ` - --logger "trx;LogFileName=test-results-dotnet-af.trx" - env: - TEST_RESULTS_DIR: ${{ runner.temp }}/TestConversations - - - name: Emit Test Conversations - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Emit-TestConversations.ps1" ` - -TestResultsDir "${{ runner.temp }}/TestConversations" - - - name: Capture Agent Logs - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Capture-AgentLogs.ps1" ` - -AgentPath "${{ env.SAMPLE_PATH }}" - - - name: Stop Agent Process - if: always() - shell: pwsh - run: | - $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" - if ($agentPid) { - & "${{ env.SCRIPTS_PATH }}/Stop-AgentProcess.ps1" -AgentPID $agentPid - } - - - name: Upload Test Results - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-results-dotnet-af - path: | - ${{ env.E2E_TESTS_PATH }}/TestResults/**/*.trx - ${{ env.E2E_TESTS_PATH }}/TestResults/**/*-logs.txt - retention-days: 7 diff --git a/.github/workflows/e2e-dotnet-semantic-kernel.yml b/.github/workflows/e2e-dotnet-semantic-kernel.yml index 1736e848..64f2412a 100644 --- a/.github/workflows/e2e-dotnet-semantic-kernel.yml +++ b/.github/workflows/e2e-dotnet-semantic-kernel.yml @@ -56,114 +56,4 @@ jobs: -Runtime "dotnet" ` -WorkingDirectory "${{ env.SAMPLE_PATH }}" - - name: Acquire Bearer Token (ROPC) - id: token - shell: pwsh - run: | - $token = & "${{ env.SCRIPTS_PATH }}/Acquire-BearerToken.ps1" ` - -ClientId "${{ secrets.MCP_CLIENT_ID }}" ` - -TenantId "${{ secrets.MCP_TENANT_ID }}" ` - -Username "${{ secrets.MCP_TEST_USERNAME }}" ` - -Password "${{ secrets.MCP_TEST_PASSWORD }}" - echo "BEARER_TOKEN=$token" >> $env:GITHUB_OUTPUT - echo "::add-mask::$token" - - - name: Copy ToolingManifest.json - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Copy-ToolingManifest.ps1" -TargetPath "${{ env.SAMPLE_PATH }}" - - - name: Generate appsettings.json - shell: pwsh - run: | - $configMappings = @{ - "AIServices:UseAzureOpenAI" = "true" - "AIServices:AzureOpenAI:Endpoint" = "${{ secrets.DOTNET_SK_AZURE_OPENAI_ENDPOINT }}" - "AIServices:AzureOpenAI:ApiKey" = "${{ secrets.DOTNET_SK_AZURE_OPENAI_API_KEY }}" - "AIServices:AzureOpenAI:DeploymentName" = "${{ secrets.DOTNET_SK_AZURE_OPENAI_DEPLOYMENT }}" - "TokenValidation:Enabled" = "false" - "TokenValidation:TenantId" = "${{ secrets.TENANT_ID }}" - "Connections:ServiceConnection:Settings:AuthType" = "ClientSecret" - "Connections:ServiceConnection:Settings:ClientId" = "${{ secrets.DOTNET_SK_CLIENT_ID }}" - "Connections:ServiceConnection:Settings:ClientSecret" = "${{ secrets.DOTNET_SK_CLIENT_SECRET }}" - "Connections:ServiceConnection:Settings:TenantId" = "${{ secrets.TENANT_ID }}" - } - & "${{ env.SCRIPTS_PATH }}/Generate-AppSettings.ps1" ` - -OutputPath "${{ env.SAMPLE_PATH }}/appsettings.json" ` - -ConfigMappings $configMappings - - - name: Start Agent - id: start-agent - shell: pwsh - run: | - $agentPid = & "${{ env.SCRIPTS_PATH }}/Start-Agent.ps1" ` - -AgentPath "${{ env.SAMPLE_PATH }}" ` - -StartCommand "dotnet run --no-build" ` - -Port ${{ env.AGENT_PORT }} ` - -BearerToken "${{ steps.token.outputs.BEARER_TOKEN }}" ` - -Environment "Development" ` - -Runtime "dotnet" - echo "AGENT_PID=$agentPid" >> $env:GITHUB_OUTPUT - - - name: Verify Agent Running - shell: pwsh - run: | - $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" - if ($agentPid) { - try { - $proc = Get-Process -Id $agentPid -ErrorAction Stop - Write-Host "Agent process (PID: $agentPid) is running: $($proc.ProcessName)" -ForegroundColor Green - } catch { - Write-Host "ERROR: Agent process (PID: $agentPid) is NOT running!" -ForegroundColor Red - throw "Agent process has stopped" - } - } - $response = Invoke-WebRequest -Uri "http://localhost:${{ env.AGENT_PORT }}/api/health" -UseBasicParsing -ErrorAction SilentlyContinue - Write-Host "Health: $($response.StatusCode)" -ForegroundColor Green - - name: Restore E2E Test Dependencies - run: dotnet restore "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" - - - name: Run .NET E2E Tests - shell: pwsh - run: | - dotnet test "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" ` - --no-restore ` - --verbosity normal ` - --logger "console;verbosity=detailed" ` - --logger "trx;LogFileName=test-results-dotnet-sk.trx" - env: - TEST_RESULTS_DIR: ${{ runner.temp }}/TestConversations - - - name: Emit Test Conversations - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Emit-TestConversations.ps1" ` - -TestResultsDir "${{ runner.temp }}/TestConversations" - - - name: Capture Agent Logs - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Capture-AgentLogs.ps1" ` - -AgentPath "${{ env.SAMPLE_PATH }}" - - - name: Stop Agent Process - if: always() - shell: pwsh - run: | - $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" - if ($agentPid) { - & "${{ env.SCRIPTS_PATH }}/Stop-AgentProcess.ps1" -AgentPID $agentPid - } - - - name: Upload Test Results - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-results-dotnet-sk - path: | - ${{ env.E2E_TESTS_PATH }}/TestResults/**/*.trx - ${{ env.E2E_TESTS_PATH }}/TestResults/**/*-logs.txt - retention-days: 7 diff --git a/.github/workflows/e2e-nodejs-langchain.yml b/.github/workflows/e2e-nodejs-langchain.yml index fd88cccc..5adf0ee1 100644 --- a/.github/workflows/e2e-nodejs-langchain.yml +++ b/.github/workflows/e2e-nodejs-langchain.yml @@ -66,121 +66,4 @@ jobs: -Runtime "nodejs" ` -WorkingDirectory "${{ env.SAMPLE_PATH }}" - - name: Acquire Bearer Token (ROPC) - id: token - shell: pwsh - run: | - $token = & "${{ env.SCRIPTS_PATH }}/Acquire-BearerToken.ps1" ` - -ClientId "${{ secrets.MCP_CLIENT_ID }}" ` - -TenantId "${{ secrets.MCP_TENANT_ID }}" ` - -Username "${{ secrets.MCP_TEST_USERNAME }}" ` - -Password "${{ secrets.MCP_TEST_PASSWORD }}" - echo "BEARER_TOKEN=$token" >> $env:GITHUB_OUTPUT - echo "::add-mask::$token" - - - name: Copy ToolingManifest.json - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Copy-ToolingManifest.ps1" -TargetPath "${{ env.SAMPLE_PATH }}" - - - name: Generate .env configuration - shell: pwsh - run: | - $configMappings = @{ - "NODE_ENV" = "development" - "AZURE_OPENAI_API_KEY" = "${{ secrets.NODEJS_OPENAI_AZURE_OPENAI_API_KEY }}" - "AZURE_OPENAI_ENDPOINT" = "${{ secrets.NODEJS_OPENAI_AZURE_OPENAI_ENDPOINT }}" - "AZURE_OPENAI_DEPLOYMENT" = "${{ secrets.NODEJS_OPENAI_AZURE_OPENAI_DEPLOYMENT }}" - "AZURE_OPENAI_API_VERSION" = "2024-12-01-preview" - "connections__service_connection__settings__authType" = "ClientSecret" - "connections__service_connection__settings__clientId" = "${{ secrets.NODEJS_OPENAI_AGENT_ID }}" - "connections__service_connection__settings__clientSecret" = "${{ secrets.NODEJS_OPENAI_CLIENT_SECRET }}" - "connections__service_connection__settings__tenantId" = "${{ secrets.TENANT_ID }}" - "connectionsMap__0__serviceUrl" = "*" - "connectionsMap__0__connection" = "service_connection" - "agentic_scopes" = "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1/.default" - } - & "${{ env.SCRIPTS_PATH }}/Generate-EnvConfig.ps1" ` - -OutputPath "${{ env.SAMPLE_PATH }}/.env" ` - -BearerToken "${{ steps.token.outputs.BEARER_TOKEN }}" ` - -Port ${{ env.AGENT_PORT }} ` - -ConfigMappings $configMappings - - - name: Start Agent - id: start-agent - shell: pwsh - run: | - $agentPid = & "${{ env.SCRIPTS_PATH }}/Start-Agent.ps1" ` - -AgentPath "${{ env.SAMPLE_PATH }}" ` - -StartCommand "node dist/index.js" ` - -Port ${{ env.AGENT_PORT }} ` - -BearerToken "${{ steps.token.outputs.BEARER_TOKEN }}" ` - -Environment "Development" ` - -Runtime "nodejs" - echo "AGENT_PID=$agentPid" >> $env:GITHUB_OUTPUT - - - name: Verify Agent Running - shell: pwsh - run: | - $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" - if ($agentPid) { - try { - $proc = Get-Process -Id $agentPid -ErrorAction Stop - Write-Host "Agent process (PID: $agentPid) is running: $($proc.ProcessName)" -ForegroundColor Green - } catch { - Write-Host "ERROR: Agent process (PID: $agentPid) is NOT running!" -ForegroundColor Red - throw "Agent process has stopped" - } - } - $agentUrl = "http://localhost:${{ env.AGENT_PORT }}" - $healthResponse = Invoke-WebRequest -Uri "$agentUrl/api/health" -Method GET -UseBasicParsing -ErrorAction SilentlyContinue - Write-Host "Health check: $($healthResponse.StatusCode)" -ForegroundColor Green - - name: Restore E2E Test Dependencies - run: dotnet restore "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" - - - name: Run .NET E2E Tests - shell: pwsh - run: | - dotnet test "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" ` - --no-restore ` - --verbosity normal ` - --logger "console;verbosity=detailed" ` - --logger "trx;LogFileName=test-results-nodejs-langchain.trx" ` - --filter "Category=HTTP" - env: - AGENT_URL: http://localhost:${{ env.AGENT_PORT }} - TEST_RESULTS_DIR: ${{ runner.temp }}/TestConversations - - - name: Emit Test Conversations - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Emit-TestConversations.ps1" ` - -TestResultsDir "${{ runner.temp }}/TestConversations" - - - name: Capture Agent Logs - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Capture-AgentLogs.ps1" ` - -AgentPath "${{ env.SAMPLE_PATH }}" - - - name: Stop Agent Process - if: always() - shell: pwsh - run: | - $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" - if ($agentPid) { - & "${{ env.SCRIPTS_PATH }}/Stop-AgentProcess.ps1" -AgentPID $agentPid - } - - - name: Upload Test Results - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-results-nodejs-langchain - path: | - ${{ env.E2E_TESTS_PATH }}/TestResults/**/*.trx - ${{ env.E2E_TESTS_PATH }}/TestResults/**/*-logs.txt - retention-days: 7 diff --git a/.github/workflows/e2e-nodejs-openai.yml b/.github/workflows/e2e-nodejs-openai.yml index 366a0f03..9c8ca775 100644 --- a/.github/workflows/e2e-nodejs-openai.yml +++ b/.github/workflows/e2e-nodejs-openai.yml @@ -66,121 +66,4 @@ jobs: -Runtime "nodejs" ` -WorkingDirectory "${{ env.SAMPLE_PATH }}" - - name: Acquire Bearer Token (ROPC) - id: token - shell: pwsh - run: | - $token = & "${{ env.SCRIPTS_PATH }}/Acquire-BearerToken.ps1" ` - -ClientId "${{ secrets.MCP_CLIENT_ID }}" ` - -TenantId "${{ secrets.MCP_TENANT_ID }}" ` - -Username "${{ secrets.MCP_TEST_USERNAME }}" ` - -Password "${{ secrets.MCP_TEST_PASSWORD }}" - echo "BEARER_TOKEN=$token" >> $env:GITHUB_OUTPUT - echo "::add-mask::$token" - - - name: Copy ToolingManifest.json - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Copy-ToolingManifest.ps1" -TargetPath "${{ env.SAMPLE_PATH }}" - - - name: Generate .env configuration - shell: pwsh - run: | - $configMappings = @{ - "NODE_ENV" = "development" - "AZURE_OPENAI_API_KEY" = "${{ secrets.NODEJS_OPENAI_AZURE_OPENAI_API_KEY }}" - "AZURE_OPENAI_ENDPOINT" = "${{ secrets.NODEJS_OPENAI_AZURE_OPENAI_ENDPOINT }}" - "AZURE_OPENAI_DEPLOYMENT" = "${{ secrets.NODEJS_OPENAI_AZURE_OPENAI_DEPLOYMENT }}" - "AZURE_OPENAI_API_VERSION" = "2024-12-01-preview" - "connections__service_connection__settings__authType" = "ClientSecret" - "connections__service_connection__settings__clientId" = "${{ secrets.NODEJS_OPENAI_AGENT_ID }}" - "connections__service_connection__settings__clientSecret" = "${{ secrets.NODEJS_OPENAI_CLIENT_SECRET }}" - "connections__service_connection__settings__tenantId" = "${{ secrets.TENANT_ID }}" - "connectionsMap__0__serviceUrl" = "*" - "connectionsMap__0__connection" = "service_connection" - "agentic_scopes" = "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1/.default" - } - & "${{ env.SCRIPTS_PATH }}/Generate-EnvConfig.ps1" ` - -OutputPath "${{ env.SAMPLE_PATH }}/.env" ` - -BearerToken "${{ steps.token.outputs.BEARER_TOKEN }}" ` - -Port ${{ env.AGENT_PORT }} ` - -ConfigMappings $configMappings - - - name: Start Agent - id: start-agent - shell: pwsh - run: | - $agentPid = & "${{ env.SCRIPTS_PATH }}/Start-Agent.ps1" ` - -AgentPath "${{ env.SAMPLE_PATH }}" ` - -StartCommand "node dist/index.js" ` - -Port ${{ env.AGENT_PORT }} ` - -BearerToken "${{ steps.token.outputs.BEARER_TOKEN }}" ` - -Environment "Development" ` - -Runtime "nodejs" - echo "AGENT_PID=$agentPid" >> $env:GITHUB_OUTPUT - - - name: Verify Agent Running - shell: pwsh - run: | - $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" - if ($agentPid) { - try { - $proc = Get-Process -Id $agentPid -ErrorAction Stop - Write-Host "Agent process (PID: $agentPid) is running: $($proc.ProcessName)" -ForegroundColor Green - } catch { - Write-Host "ERROR: Agent process (PID: $agentPid) is NOT running!" -ForegroundColor Red - throw "Agent process has stopped" - } - } - $agentUrl = "http://localhost:${{ env.AGENT_PORT }}" - $healthResponse = Invoke-WebRequest -Uri "$agentUrl/api/health" -Method GET -UseBasicParsing -ErrorAction SilentlyContinue - Write-Host "Health check: $($healthResponse.StatusCode)" -ForegroundColor Green - - name: Restore E2E Test Dependencies - run: dotnet restore "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" - - - name: Run .NET E2E Tests - shell: pwsh - run: | - dotnet test "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" ` - --no-restore ` - --verbosity normal ` - --logger "console;verbosity=detailed" ` - --logger "trx;LogFileName=test-results-nodejs-openai.trx" ` - --filter "Category=HTTP" - env: - AGENT_URL: http://localhost:${{ env.AGENT_PORT }} - TEST_RESULTS_DIR: ${{ runner.temp }}/TestConversations - - - name: Emit Test Conversations - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Emit-TestConversations.ps1" ` - -TestResultsDir "${{ runner.temp }}/TestConversations" - - - name: Capture Agent Logs - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Capture-AgentLogs.ps1" ` - -AgentPath "${{ env.SAMPLE_PATH }}" - - - name: Stop Agent Process - if: always() - shell: pwsh - run: | - $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" - if ($agentPid) { - & "${{ env.SCRIPTS_PATH }}/Stop-AgentProcess.ps1" -AgentPID $agentPid - } - - - name: Upload Test Results - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-results-nodejs-openai - path: | - ${{ env.E2E_TESTS_PATH }}/TestResults/**/*.trx - ${{ env.E2E_TESTS_PATH }}/TestResults/**/*-logs.txt - retention-days: 7 diff --git a/.github/workflows/e2e-python-agent-framework.yml b/.github/workflows/e2e-python-agent-framework.yml index a4f3c05f..342a550a 100644 --- a/.github/workflows/e2e-python-agent-framework.yml +++ b/.github/workflows/e2e-python-agent-framework.yml @@ -60,133 +60,4 @@ jobs: -Runtime "python" ` -WorkingDirectory "${{ env.SAMPLE_PATH }}" - - name: Acquire Bearer Token (ROPC) - id: token - shell: pwsh - run: | - $token = & "${{ env.SCRIPTS_PATH }}/Acquire-BearerToken.ps1" ` - -ClientId "${{ secrets.MCP_CLIENT_ID }}" ` - -TenantId "${{ secrets.MCP_TENANT_ID }}" ` - -Username "${{ secrets.MCP_TEST_USERNAME }}" ` - -Password "${{ secrets.MCP_TEST_PASSWORD }}" - echo "BEARER_TOKEN=$token" >> $env:GITHUB_OUTPUT - echo "::add-mask::$token" - - - name: Copy ToolingManifest.json - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Copy-ToolingManifest.ps1" -TargetPath "${{ env.SAMPLE_PATH }}" - - - name: Generate .env configuration - shell: pwsh - run: | - $configMappings = @{ - "AZURE_OPENAI_API_KEY" = "${{ secrets.PYTHON_OPENAI_AZURE_OPENAI_API_KEY }}" - "AZURE_OPENAI_ENDPOINT" = "${{ secrets.PYTHON_OPENAI_AZURE_OPENAI_ENDPOINT }}" - "AZURE_OPENAI_DEPLOYMENT" = "${{ secrets.PYTHON_OPENAI_AZURE_OPENAI_DEPLOYMENT }}" - "AZURE_OPENAI_API_VERSION" = "2024-12-01-preview" - "USE_AGENTIC_AUTH" = "false" - "MCP_PLATFORM_ENDPOINT" = "https://agent365.svc.cloud.microsoft" - "AGENT_ID" = "${{ secrets.PYTHON_OPENAI_AGENT_ID }}" - "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__AUTHTYPE" = "ClientSecret" - "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID" = "${{ secrets.PYTHON_OPENAI_AGENT_ID }}" - "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET" = "${{ secrets.PYTHON_OPENAI_CLIENT_SECRET }}" - "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID" = "${{ secrets.TENANT_ID }}" - "CONNECTIONSMAP__0__SERVICEURL" = "*" - "CONNECTIONSMAP__0__CONNECTION" = "SERVICE_CONNECTION" - "ENABLE_OBSERVABILITY" = "true" - } - & "${{ env.SCRIPTS_PATH }}/Generate-EnvConfig.ps1" ` - -OutputPath "${{ env.SAMPLE_PATH }}/.env" ` - -BearerToken "${{ steps.token.outputs.BEARER_TOKEN }}" ` - -Port ${{ env.AGENT_PORT }} ` - -ConfigMappings $configMappings - - - name: Start Agent - id: start-agent - shell: pwsh - run: | - $agentPid = & "${{ env.SCRIPTS_PATH }}/Start-Agent.ps1" ` - -AgentPath "${{ env.SAMPLE_PATH }}" ` - -StartCommand "uv run python start_with_generic_host.py" ` - -Port ${{ env.AGENT_PORT }} ` - -BearerToken "${{ steps.token.outputs.BEARER_TOKEN }}" ` - -Environment "Development" ` - -Runtime "python" - echo "AGENT_PID=$agentPid" >> $env:GITHUB_OUTPUT - - - name: Verify Agent Running - shell: pwsh - run: | - Write-Host "=== Verifying Agent ===" -ForegroundColor Cyan - - $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" - Write-Host "Checking agent process (PID: $agentPid)..." -ForegroundColor Gray - - if ($agentPid) { - try { - $proc = Get-Process -Id $agentPid -ErrorAction Stop - Write-Host "Agent process (PID: $agentPid) is running: $($proc.ProcessName)" -ForegroundColor Green - } catch { - Write-Host "ERROR: Agent process (PID: $agentPid) is NOT running!" -ForegroundColor Red - - $logFile = "${{ env.SAMPLE_PATH }}/agent.log" - if (Test-Path $logFile) { - Write-Host "Agent logs:" -ForegroundColor Yellow - Get-Content $logFile - } - throw "Agent process has stopped" - } - } - - $agentUrl = "http://localhost:${{ env.AGENT_PORT }}" - Write-Host "Verifying agent at $agentUrl..." -ForegroundColor Gray - $healthResponse = Invoke-WebRequest -Uri "$agentUrl/api/health" -Method GET -UseBasicParsing -ErrorAction SilentlyContinue - Write-Host "Health check: $($healthResponse.StatusCode)" -ForegroundColor Green - - - name: Restore E2E Test Dependencies - shell: pwsh - run: | - dotnet restore "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" - - - name: Run .NET E2E Tests - shell: pwsh - run: | - dotnet test "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" ` - --no-restore ` - --filter "Category=HTTP" ` - --logger "trx;LogFileName=test-results.trx" ` - --results-directory "${{ runner.temp }}/TestResults" - env: - AGENT_PORT: ${{ env.AGENT_PORT }} - AGENT_URL: http://localhost:${{ env.AGENT_PORT }} - TEST_RESULTS_DIR: ${{ runner.temp }}/TestConversations - - - name: Upload Test Results - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-results-python-agent-framework - path: ${{ runner.temp }}/TestResults/ - retention-days: 30 - - name: Emit Test Conversations - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Emit-TestConversations.ps1" ` - -TestResultsDir "${{ runner.temp }}/TestConversations" - - - name: Capture Agent Logs - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Capture-AgentLogs.ps1" -AgentPath "${{ env.SAMPLE_PATH }}" - - - name: Cleanup Agent Process - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Stop-AgentProcess.ps1" ` - -AgentPID "${{ steps.start-agent.outputs.AGENT_PID }}" ` - -Port ${{ env.AGENT_PORT }} diff --git a/.github/workflows/e2e-python-openai.yml b/.github/workflows/e2e-python-openai.yml index e666053c..06b449c5 100644 --- a/.github/workflows/e2e-python-openai.yml +++ b/.github/workflows/e2e-python-openai.yml @@ -60,133 +60,4 @@ jobs: -Runtime "python" ` -WorkingDirectory "${{ env.SAMPLE_PATH }}" - - name: Acquire Bearer Token (ROPC) - id: token - shell: pwsh - run: | - $token = & "${{ env.SCRIPTS_PATH }}/Acquire-BearerToken.ps1" ` - -ClientId "${{ secrets.MCP_CLIENT_ID }}" ` - -TenantId "${{ secrets.MCP_TENANT_ID }}" ` - -Username "${{ secrets.MCP_TEST_USERNAME }}" ` - -Password "${{ secrets.MCP_TEST_PASSWORD }}" - echo "BEARER_TOKEN=$token" >> $env:GITHUB_OUTPUT - echo "::add-mask::$token" - - - name: Copy ToolingManifest.json - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Copy-ToolingManifest.ps1" -TargetPath "${{ env.SAMPLE_PATH }}" - - - name: Generate .env configuration - shell: pwsh - run: | - $configMappings = @{ - "AZURE_OPENAI_API_KEY" = "${{ secrets.PYTHON_OPENAI_AZURE_OPENAI_API_KEY }}" - "AZURE_OPENAI_ENDPOINT" = "${{ secrets.PYTHON_OPENAI_AZURE_OPENAI_ENDPOINT }}" - "AZURE_OPENAI_DEPLOYMENT" = "${{ secrets.PYTHON_OPENAI_AZURE_OPENAI_DEPLOYMENT }}" - "AZURE_OPENAI_API_VERSION" = "2024-12-01-preview" - "USE_AGENTIC_AUTH" = "false" - "MCP_PLATFORM_ENDPOINT" = "https://agent365.svc.cloud.microsoft" - "AGENT_ID" = "${{ secrets.PYTHON_OPENAI_AGENT_ID }}" - "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__AUTHTYPE" = "ClientSecret" - "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID" = "${{ secrets.PYTHON_OPENAI_AGENT_ID }}" - "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET" = "${{ secrets.PYTHON_OPENAI_CLIENT_SECRET }}" - "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID" = "${{ secrets.TENANT_ID }}" - "CONNECTIONSMAP__0__SERVICEURL" = "*" - "CONNECTIONSMAP__0__CONNECTION" = "SERVICE_CONNECTION" - "ENABLE_OBSERVABILITY" = "true" - } - & "${{ env.SCRIPTS_PATH }}/Generate-EnvConfig.ps1" ` - -OutputPath "${{ env.SAMPLE_PATH }}/.env" ` - -BearerToken "${{ steps.token.outputs.BEARER_TOKEN }}" ` - -Port ${{ env.AGENT_PORT }} ` - -ConfigMappings $configMappings - - - name: Start Agent - id: start-agent - shell: pwsh - run: | - $agentPid = & "${{ env.SCRIPTS_PATH }}/Start-Agent.ps1" ` - -AgentPath "${{ env.SAMPLE_PATH }}" ` - -StartCommand "uv run python start_with_generic_host.py" ` - -Port ${{ env.AGENT_PORT }} ` - -BearerToken "${{ steps.token.outputs.BEARER_TOKEN }}" ` - -Environment "Development" ` - -Runtime "python" - echo "AGENT_PID=$agentPid" >> $env:GITHUB_OUTPUT - - - name: Verify Agent Running - shell: pwsh - run: | - Write-Host "=== Verifying Agent ===" -ForegroundColor Cyan - - $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" - Write-Host "Checking agent process (PID: $agentPid)..." -ForegroundColor Gray - - if ($agentPid) { - try { - $proc = Get-Process -Id $agentPid -ErrorAction Stop - Write-Host "Agent process (PID: $agentPid) is running: $($proc.ProcessName)" -ForegroundColor Green - } catch { - Write-Host "ERROR: Agent process (PID: $agentPid) is NOT running!" -ForegroundColor Red - - $logFile = "${{ env.SAMPLE_PATH }}/agent.log" - if (Test-Path $logFile) { - Write-Host "Agent logs:" -ForegroundColor Yellow - Get-Content $logFile - } - throw "Agent process has stopped" - } - } - - $agentUrl = "http://localhost:${{ env.AGENT_PORT }}" - Write-Host "Verifying agent at $agentUrl..." -ForegroundColor Gray - $healthResponse = Invoke-WebRequest -Uri "$agentUrl/api/health" -Method GET -UseBasicParsing -ErrorAction SilentlyContinue - Write-Host "Health check: $($healthResponse.StatusCode)" -ForegroundColor Green - - - name: Restore E2E Test Dependencies - shell: pwsh - run: | - dotnet restore "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" - - - name: Run .NET E2E Tests - shell: pwsh - run: | - dotnet test "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" ` - --no-restore ` - --filter "Category=HTTP" ` - --logger "trx;LogFileName=test-results.trx" ` - --results-directory "${{ runner.temp }}/TestResults" - env: - AGENT_PORT: ${{ env.AGENT_PORT }} - AGENT_URL: http://localhost:${{ env.AGENT_PORT }} - TEST_RESULTS_DIR: ${{ runner.temp }}/TestConversations - - - name: Upload Test Results - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-results-python-openai - path: ${{ runner.temp }}/TestResults/ - retention-days: 30 - - name: Emit Test Conversations - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Emit-TestConversations.ps1" ` - -TestResultsDir "${{ runner.temp }}/TestConversations" - - - name: Capture Agent Logs - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Capture-AgentLogs.ps1" -AgentPath "${{ env.SAMPLE_PATH }}" - - - name: Cleanup Agent Process - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Stop-AgentProcess.ps1" ` - -AgentPID "${{ steps.start-agent.outputs.AGENT_PID }}" ` - -Port ${{ env.AGENT_PORT }} diff --git a/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs b/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs index e189e7b5..05c86fbc 100644 --- a/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs +++ b/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs @@ -1,9 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using Agent365AgentFrameworkSampleAgent.telemetry; using Agent365AgentFrameworkSampleAgent.Tools; -using Microsoft.Agents.A365.Observability.Hosting.Caching; using Microsoft.Agents.A365.Runtime.Utils; using Microsoft.Agents.A365.Tooling.Extensions.AgentFramework.Services; using Microsoft.Agents.AI; @@ -15,6 +13,7 @@ using Microsoft.Agents.Core.Serialization; using Microsoft.Extensions.AI; using System.Collections.Concurrent; +using System.Text; using System.Text.Json; namespace Agent365AgentFrameworkSampleAgent.Agent @@ -39,7 +38,7 @@ You will speak like a friendly and professional virtual assistant. You may ask follow up questions until you have enough information to answer the customers question, but once you have the current weather or a forecast, make sure to format it nicely in text. - For current weather, Use the {{WeatherLookupTool.GetCurrentWeatherForLocation}}, you should include the current temperature, low and high temperatures, wind speed, humidity, and a short description of the weather. - For forecast's, Use the {{WeatherLookupTool.GetWeatherForecastForLocation}}, you should report on the next 5 days, including the current day, and include the date, high and low temperatures, and a short description of the weather. - - You should use the {{DateTimePlugin.GetDateTime}} to get the current date and time. + - You should use the {{DateTimeFunctionTool.GetCurrentDateTime}} to get the current date and time. Otherwise you should use the tools available to you to help answer the user's questions. """; @@ -59,7 +58,6 @@ private static string GetAgentInstructions(string? userName) private readonly IChatClient? _chatClient = null; private readonly IConfiguration? _configuration = null; - private readonly IExporterTokenCache? _agentTokenCache = null; private readonly ILogger? _logger = null; private readonly IMcpToolRegistrationService? _toolService = null; // Setup reusable auto sign-in handlers for user authorization (configurable via appsettings.json) @@ -98,13 +96,11 @@ private static bool ShouldSkipToolingOnErrors() public MyAgent(AgentApplicationOptions options, IChatClient chatClient, IConfiguration configuration, - IExporterTokenCache agentTokenCache, IMcpToolRegistrationService toolService, ILogger logger) : base(options) { _chatClient = chatClient; _configuration = configuration; - _agentTokenCache = agentTokenCache; _logger = logger; _toolService = toolService; @@ -133,19 +129,11 @@ public MyAgent(AgentApplicationOptions options, protected async Task WelcomeMessageAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken) { - await AgentMetrics.InvokeObservedAgentOperation( - "WelcomeMessage", - turnContext, - async () => + foreach (var _ in (turnContext.Activity.MembersAdded ?? []) + .Where(m => m.Id != turnContext.Activity.Recipient.Id)) { - foreach (ChannelAccount member in turnContext.Activity.MembersAdded) - { - if (member.Id != turnContext.Activity.Recipient.Id) - { - await turnContext.SendActivityAsync(AgentWelcomeMessage); - } - } - }); + await turnContext.SendActivityAsync(AgentWelcomeMessage); + } } /// @@ -154,26 +142,20 @@ await AgentMetrics.InvokeObservedAgentOperation( /// protected async Task OnInstallationUpdateAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken) { - await AgentMetrics.InvokeObservedAgentOperation( - "InstallationUpdate", - turnContext, - async () => - { - _logger?.LogInformation( - "InstallationUpdate received — Action: '{Action}', DisplayName: '{Name}', UserId: '{Id}'", - turnContext.Activity.Action ?? "(none)", - turnContext.Activity.From?.Name ?? "(unknown)", - turnContext.Activity.From?.Id ?? "(unknown)"); + _logger?.LogInformation( + "InstallationUpdate received — Action: '{Action}', DisplayName: '{Name}', UserId: '{Id}'", + turnContext.Activity.Action ?? "(none)", + turnContext.Activity.From?.Name ?? "(unknown)", + turnContext.Activity.From?.Id ?? "(unknown)"); - if (turnContext.Activity.Action == InstallationUpdateActionTypes.Add) - { - await turnContext.SendActivityAsync(MessageFactory.Text(AgentHireMessage), cancellationToken); - } - else if (turnContext.Activity.Action == InstallationUpdateActionTypes.Remove) - { - await turnContext.SendActivityAsync(MessageFactory.Text(AgentFarewellMessage), cancellationToken); - } - }); + if (turnContext.Activity.Action == InstallationUpdateActionTypes.Add) + { + await turnContext.SendActivityAsync(MessageFactory.Text(AgentHireMessage), cancellationToken); + } + else if (turnContext.Activity.Action == InstallationUpdateActionTypes.Remove) + { + await turnContext.SendActivityAsync(MessageFactory.Text(AgentFarewellMessage), cancellationToken); + } } /// @@ -201,99 +183,89 @@ protected async Task OnMessageAsync(ITurnContext turnContext, ITurnState turnSta // Select the appropriate auth handler based on request type // For agentic requests, use the agentic auth handler // For non-agentic requests, use OBO auth handler (supports bearer token or configured auth) - string? ObservabilityAuthHandlerName; string? ToolAuthHandlerName; if (turnContext.IsAgenticRequest()) { - ObservabilityAuthHandlerName = ToolAuthHandlerName = AgenticAuthHandlerName; + ToolAuthHandlerName = AgenticAuthHandlerName; } else { // Non-agentic: use OBO auth handler if configured - ObservabilityAuthHandlerName = ToolAuthHandlerName = OboAuthHandlerName; + ToolAuthHandlerName = OboAuthHandlerName; } - await A365OtelWrapper.InvokeObservedAgentOperation( - "MessageProcessor", - turnContext, - turnState, - _agentTokenCache, - UserAuthorization, - ObservabilityAuthHandlerName ?? string.Empty, - _logger, - async () => + // Send an immediate acknowledgment — this arrives as a separate message before the LLM response. + // Each SendActivityAsync call produces a discrete Teams message, enabling the multiple-messages pattern. + // NOTE: For Teams agentic identities, streaming is buffered into a single message by the SDK; + // use SendActivityAsync for any messages that must arrive immediately. + await turnContext.SendActivityAsync(MessageFactory.Text("Got it — working on it…"), cancellationToken).ConfigureAwait(false); + + // Send typing indicator immediately on the main thread (awaited so it arrives before the LLM call starts). + await turnContext.SendActivityAsync(Activity.CreateTypingActivity(), cancellationToken).ConfigureAwait(false); + + // Background loop refreshes the "..." animation every ~4s (it times out after ~5s). + // Only visible in 1:1 and small group chats. + using var typingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var typingTask = Task.Run(async () => { - // Send an immediate acknowledgment — this arrives as a separate message before the LLM response. - // Each SendActivityAsync call produces a discrete Teams message, enabling the multiple-messages pattern. - // NOTE: For Teams agentic identities, streaming is buffered into a single message by the SDK; - // use SendActivityAsync for any messages that must arrive immediately. - await turnContext.SendActivityAsync(MessageFactory.Text("Got it — working on it…"), cancellationToken).ConfigureAwait(false); - - // Send typing indicator immediately on the main thread (awaited so it arrives before the LLM call starts). - await turnContext.SendActivityAsync(Activity.CreateTypingActivity(), cancellationToken).ConfigureAwait(false); - - // Background loop refreshes the "..." animation every ~4s (it times out after ~5s). - // Only visible in 1:1 and small group chats. - using var typingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - var typingTask = Task.Run(async () => + try { - try + while (!typingCts.IsCancellationRequested) { - while (!typingCts.IsCancellationRequested) - { - await Task.Delay(TimeSpan.FromSeconds(4), typingCts.Token).ConfigureAwait(false); - await turnContext.SendActivityAsync(Activity.CreateTypingActivity(), typingCts.Token).ConfigureAwait(false); - } + await Task.Delay(TimeSpan.FromSeconds(4), typingCts.Token).ConfigureAwait(false); + await turnContext.SendActivityAsync(Activity.CreateTypingActivity(), typingCts.Token).ConfigureAwait(false); } - catch (OperationCanceledException) { /* expected on cancel */ } - }, typingCts.Token); + } + catch (OperationCanceledException) { /* expected on cancel */ } + }, typingCts.Token); - // StreamingResponse is best-effort: in Teams with agentic identity the SDK may buffer/downscale it. - // The ack + typing loop above handle the immediate UX; streaming remains for non-Teams / WebChat clients. - await turnContext.StreamingResponse.QueueInformativeUpdateAsync("Just a moment please..").ConfigureAwait(false); - try - { - var userText = turnContext.Activity.Text?.Trim() ?? string.Empty; - var _agent = await GetClientAgent(turnContext, turnState, _toolService, ToolAuthHandlerName); + // StreamingResponse is best-effort: in Teams with agentic identity the SDK may buffer/downscale it. + // The ack + typing loop above handle the immediate UX; streaming remains for non-Teams / WebChat clients. + await turnContext.StreamingResponse.QueueInformativeUpdateAsync("Just a moment please..").ConfigureAwait(false); + try + { + var userTextBuilder = new StringBuilder(turnContext.Activity.Text?.Trim() ?? string.Empty); + var _agent = await GetClientAgent(turnContext, turnState, _toolService, ToolAuthHandlerName); - // Read or Create the conversation thread for this conversation. - AgentThread? thread = GetConversationThread(_agent, turnState); + // Read or Create the conversation session for this conversation. + AgentSession? session = await GetConversationSessionAsync(_agent, turnState, cancellationToken); - if (turnContext?.Activity?.Attachments?.Count > 0) + if (turnContext.Activity?.Attachments?.Count > 0) + { + foreach (var attachment in turnContext.Activity.Attachments) { - foreach (var attachment in turnContext.Activity.Attachments) + if (attachment.ContentType == "application/vnd.microsoft.teams.file.download.info" && !string.IsNullOrEmpty(attachment.ContentUrl)) { - if (attachment.ContentType == "application/vnd.microsoft.teams.file.download.info" && !string.IsNullOrEmpty(attachment.ContentUrl)) - { - userText += $"\n\n[User has attached a file: {attachment.Name}. The file can be downloaded from {attachment.ContentUrl}]"; - } + userTextBuilder.Append($"\n\n[User has attached a file: {attachment.Name}. The file can be downloaded from {attachment.ContentUrl}]"); } } + } + var userText = userTextBuilder.ToString(); - // Stream the response back to the user as we receive it from the agent. - await foreach (var response in _agent!.RunStreamingAsync(userText, thread, cancellationToken: cancellationToken)) + // Stream the response back to the user as we receive it from the agent. + await foreach (var response in _agent!.RunStreamingAsync(userText, session, cancellationToken: cancellationToken)) + { + if (response.Role == ChatRole.Assistant && !string.IsNullOrEmpty(response.Text)) { - if (response.Role == ChatRole.Assistant && !string.IsNullOrEmpty(response.Text)) - { - turnContext?.StreamingResponse.QueueTextChunk(response.Text); - } + turnContext.StreamingResponse.QueueTextChunk(response.Text); } - turnState.Conversation.SetValue("conversation.threadInfo", ProtocolJsonSerializer.ToJson(thread.Serialize())); } - finally + var serializedSession = await _agent!.SerializeSessionAsync(session!); + turnState.Conversation.SetValue("conversation.threadInfo", ProtocolJsonSerializer.ToJson(serializedSession)); + } + finally + { + typingCts.Cancel(); + try + { + await typingTask.ConfigureAwait(false); + } + catch (OperationCanceledException) { - typingCts.Cancel(); - try - { - await typingTask.ConfigureAwait(false); - } - catch (OperationCanceledException) - { - // Expected: typingTask is canceled when typingCts is canceled; no further action required. - } - await turnContext.StreamingResponse.EndStreamAsync(cancellationToken).ConfigureAwait(false); + // Expected: typingTask is canceled when typingCts is canceled; no further action required. } - }); + await turnContext.StreamingResponse.EndStreamAsync(cancellationToken).ConfigureAwait(false); + } } @@ -340,7 +312,8 @@ await A365OtelWrapper.InvokeObservedAgentOperation( // Create the local tools: var toolList = new List(); WeatherLookupTool weatherLookupTool = new(context, _configuration!); - toolList.Add(AIFunctionFactory.Create(DateTimeFunctionTool.getDate)); + DateTimeFunctionTool dateTimeTool = new(); + toolList.Add(AIFunctionFactory.Create(dateTimeTool.GetCurrentDateTime)); toolList.Add(AIFunctionFactory.Create(weatherLookupTool.GetCurrentWeatherForLocation)); toolList.Add(AIFunctionFactory.Create(weatherLookupTool.GetWeatherForecastForLocation)); @@ -391,28 +364,28 @@ await A365OtelWrapper.InvokeObservedAgentOperation( } } - // Create Chat Options with tools: + // Create Chat Options with tools and instructions: var toolOptions = new ChatOptions { Temperature = (float?)0.2, - Tools = toolList + Tools = toolList, + Instructions = GetAgentInstructions(displayName) }; // Create the chat Client passing in agent instructions and tools: return new ChatClientAgent(_chatClient!, new ChatClientAgentOptions { - Instructions = GetAgentInstructions(displayName), ChatOptions = toolOptions, - ChatMessageStoreFactory = ctx => + ChatHistoryProvider = new InMemoryChatHistoryProvider(new InMemoryChatHistoryProviderOptions { #pragma warning disable MEAI001 // MessageCountingChatReducer is for evaluation purposes only and is subject to change or removal in future updates - return new InMemoryChatMessageStore(new MessageCountingChatReducer(10), ctx.SerializedState, ctx.JsonSerializerOptions); + ChatReducer = new MessageCountingChatReducer(10) #pragma warning restore MEAI001 // MessageCountingChatReducer is for evaluation purposes only and is subject to change or removal in future updates - } + }) }) .AsBuilder() - .UseOpenTelemetry(sourceName: AgentMetrics.SourceName, (cfg) => cfg.EnableSensitiveData = true) + .UseOpenTelemetry(sourceName: null, (cfg) => cfg.EnableSensitiveData = true) .Build(); } @@ -422,21 +395,19 @@ await A365OtelWrapper.InvokeObservedAgentOperation( /// ChatAgent /// State Manager for the Agent. /// - private static AgentThread GetConversationThread(AIAgent? agent, ITurnState turnState) + private static async Task GetConversationSessionAsync(AIAgent? agent, ITurnState turnState, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(agent); - AgentThread thread; string? agentThreadInfo = turnState.Conversation.GetValue("conversation.threadInfo", () => null); if (string.IsNullOrEmpty(agentThreadInfo)) { - thread = agent.GetNewThread(); + return await agent.CreateSessionAsync(cancellationToken); } else { JsonElement ele = ProtocolJsonSerializer.ToObject(agentThreadInfo); - thread = agent.DeserializeThread(ele); + return await agent.DeserializeSessionAsync(ele, cancellationToken: cancellationToken); } - return thread; } private string GetToolCacheKey(ITurnState turnState) @@ -450,5 +421,6 @@ private string GetToolCacheKey(ITurnState turnState) } return userToolCacheKey; } + } } diff --git a/dotnet/agent-framework/sample-agent/AgentFrameworkSampleAgent.csproj b/dotnet/agent-framework/sample-agent/AgentFrameworkSampleAgent.csproj index 8b72a305..1445bed6 100644 --- a/dotnet/agent-framework/sample-agent/AgentFrameworkSampleAgent.csproj +++ b/dotnet/agent-framework/sample-agent/AgentFrameworkSampleAgent.csproj @@ -13,20 +13,20 @@ - + - - + + - - - - - - + + + + + + diff --git a/dotnet/agent-framework/sample-agent/Program.cs b/dotnet/agent-framework/sample-agent/Program.cs index ad55e9ce..05a0c940 100644 --- a/dotnet/agent-framework/sample-agent/Program.cs +++ b/dotnet/agent-framework/sample-agent/Program.cs @@ -3,7 +3,6 @@ using Agent365AgentFrameworkSampleAgent; using Agent365AgentFrameworkSampleAgent.Agent; -using Agent365AgentFrameworkSampleAgent.telemetry; using Azure; using Azure.AI.OpenAI; using Microsoft.Agents.A365.Tooling.Extensions.AgentFramework.Services; @@ -27,6 +26,12 @@ o.Exporters = builder.Environment.IsDevelopment() ? ExportTarget.Agent365 | ExportTarget.Console : ExportTarget.Agent365; + + // Agent365-only export suppresses infrastructure instrumentation by default. + // Re-enable explicitly so HTTP calls (Azure OpenAI, auth, Teams) appear in traces. + o.Instrumentation.EnableAspNetCoreInstrumentation = true; + o.Instrumentation.EnableHttpClientInstrumentation = true; + o.Instrumentation.EnableAzureSdkInstrumentation = true; }); builder.Configuration.AddUserSecrets(Assembly.GetExecutingAssembly()); @@ -84,7 +89,7 @@ .AsIChatClient() .AsBuilder() .UseFunctionInvocation() - .UseOpenTelemetry(sourceName: AgentMetrics.SourceName, configure: (cfg) => cfg.EnableSensitiveData = true) + .UseOpenTelemetry(sourceName: null, configure: (cfg) => cfg.EnableSensitiveData = true) .Build(); }); @@ -106,10 +111,7 @@ // Map the /api/messages endpoint to the AgentApplication app.MapPost("/api/messages", async (HttpRequest request, HttpResponse response, IAgentHttpAdapter adapter, IAgent agent, CancellationToken cancellationToken) => { - await AgentMetrics.InvokeObservedHttpOperation("agent.process_message", async () => - { - await adapter.ProcessAsync(request, response, agent, cancellationToken); - }).ConfigureAwait(false); + await adapter.ProcessAsync(request, response, agent, cancellationToken); }); // Health check endpoint for CI/CD pipelines and monitoring diff --git a/dotnet/agent-framework/sample-agent/README.md b/dotnet/agent-framework/sample-agent/README.md index d98b6c7a..726f6629 100644 --- a/dotnet/agent-framework/sample-agent/README.md +++ b/dotnet/agent-framework/sample-agent/README.md @@ -116,6 +116,46 @@ finally > **Note**: Typing indicators are only visible in 1:1 chats and small group chats — not in channels. +## Observability + +This sample uses the [`Microsoft.OpenTelemetry`](https://www.nuget.org/packages/Microsoft.OpenTelemetry) distro, configured in `Program.cs` with a single call: + +```csharp +builder.UseMicrosoftOpenTelemetry(o => +{ + o.Exporters = builder.Environment.IsDevelopment() + ? ExportTarget.Agent365 | ExportTarget.Console + : ExportTarget.Agent365; + + o.Instrumentation.EnableAspNetCoreInstrumentation = true; + o.Instrumentation.EnableHttpClientInstrumentation = true; + o.Instrumentation.EnableAzureSdkInstrumentation = true; +}); +``` + +This produces the following spans automatically — no custom tracing code required: + +| Span | Source | What it captures | +|---|---|---| +| `POST /api/messages` | ASP.NET Core | Inbound request — method, path, status code | +| `POST login.microsoftonline.com` | HttpClient | MSAL token acquisition | +| `POST smba.trafficmanager.net` | HttpClient | Outbound Teams messages | +| `POST …openai.azure.com/…/chat/completions` | HttpClient | Raw Azure OpenAI HTTP call | +| `chat ` | `Microsoft.Extensions.AI` | Full `gen_ai.*` semantics — model, tool definitions, system prompt, input/output messages, token counts, finish reason | +| `invoke_agent ` | `Microsoft.Agents.AI` | Agent-level span — agent ID, input/output, token counts | + +The `gen_ai.*` attributes on the `chat` span come from `.UseOpenTelemetry()` on the `ChatClientAgent` builder in `Program.cs`. This is the only non-distro observability call in the sample and is worth keeping — it enriches every LLM call with structured semantic data at no cost. + +### Service Name + +By default the OTel SDK sets `service.name` to `unknown_service:`. Set the standard `OTEL_SERVICE_NAME` environment variable to give your service a meaningful name in traces: + +```bash +OTEL_SERVICE_NAME="Agent Framework Sample" +``` + +Set this in your local launch profile, deployment environment, or container configuration to match your service catalog. + ## Running the Agent To set up and test this agent, refer to the [Configure Agent Testing](https://learn.microsoft.com/en-us/microsoft-agent-365/developer/testing?tabs=dotnet) guide for complete instructions. diff --git a/dotnet/agent-framework/sample-agent/Tools/DateTimeFunctionTool.cs b/dotnet/agent-framework/sample-agent/Tools/DateTimeFunctionTool.cs index 983cf264..154618f1 100644 --- a/dotnet/agent-framework/sample-agent/Tools/DateTimeFunctionTool.cs +++ b/dotnet/agent-framework/sample-agent/Tools/DateTimeFunctionTool.cs @@ -1,17 +1,16 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.ComponentModel; namespace Agent365AgentFrameworkSampleAgent.Tools { - public static class DateTimeFunctionTool + public class DateTimeFunctionTool { [Description("Use this tool to get the current date and time")] - public static string getDate(string input) + public string GetCurrentDateTime() { - string date = DateTimeOffset.Now.ToString("D", null); - return date; + return DateTimeOffset.Now.ToString("D", null); } } } diff --git a/dotnet/agent-framework/sample-agent/appsettings.json b/dotnet/agent-framework/sample-agent/appsettings.json index ea668f2d..889c45da 100644 --- a/dotnet/agent-framework/sample-agent/appsettings.json +++ b/dotnet/agent-framework/sample-agent/appsettings.json @@ -14,7 +14,8 @@ "Settings": { "Scopes": [ "https://graph.microsoft.com/.default" - ] + ], + "AlternateBlueprintConnectionName": "ServiceConnection" } } // To use OBO auth instead, uncomment the following lines. @@ -35,7 +36,7 @@ "TokenValidation": { "Audiences": [ - "{{ClientId}}" // this is the Client ID used for the Azure Bot + "{{BOT_ID}}" // Agent Identity App ID (Enterprise App in Entra, NOT the Blueprint) ] }, @@ -54,10 +55,11 @@ "Settings": { "AuthType": "UserManagedIdentity", // this is the AuthType for the connection, valid values can be found in Microsoft.Agents.Authentication.Msal.Model.AuthTypes. "AuthorityEndpoint": "https://login.microsoftonline.com/{{BOT_TENANT_ID}}", - "ClientId": "{{BOT_ID}}", // this is the BluePrint Client ID used for the connection. + "ClientId": "{{BLUEPRINT_ID}}", // Blueprint App ID — from a365.generated.config.json: agentBlueprintId "Scopes": [ "5a807f24-c9de-44ee-a3a7-329e88a00ffc/.default" - ] + ], + "AgentId": "{{BOT_ID}}" // Agent Identity App ID — Enterprise App in Entra (different from Blueprint above) } } }, @@ -69,10 +71,19 @@ ], "AIServices": { "AzureOpenAI": { - "DeploymentName": "----", // This is the Deployment (as opposed to model) Name of the Azure OpenAI model - "Endpoint": "----", // This is the Endpoint of the Azure OpenAI model deployment - "ApiKey": "----" // This is the API Key of the Azure OpenAI model deployment + "DeploymentName": "gpt-4o", // This is the Deployment (as opposed to model) Name of the Azure OpenAI model + "Endpoint": "<>", // This is the Endpoint of the Azure OpenAI model deployment + "ApiKey": "<>" // This is the API Key of the Azure OpenAI model deployment } }, - "OpenWeatherApiKey": "----" //https://openweathermap.org/price - You will need to create a free account to get an API key (its at the bottom of the page). -} + "OpenWeatherApiKey": "<>", //https://openweathermap.org/price - You will need to create a free account to get an API key (its at the bottom of the page). + "Agent365Observability": { + "AgentId": "{{BOT_ID}}", // this is the Agent ID used for observability reporting + "AgentName": "My Agent", + "AgentDescription": "My agent description", + "TenantId": "{{BOT_TENANT_ID}}", + "AgentBlueprintId": "{{BLUEPRINT_ID}}", // this is the Blueprint ID for the agent + "ClientId": "{{BLUEPRINT_ID}}", // Blueprint App ID — used by ObservabilityTokenService to acquire tokens via the FMI chain + "ClientSecret": "<>" + } +} \ No newline at end of file diff --git a/dotnet/agent-framework/sample-agent/docs/design.md b/dotnet/agent-framework/sample-agent/docs/design.md index c3e43533..bcdf8a76 100644 --- a/dotnet/agent-framework/sample-agent/docs/design.md +++ b/dotnet/agent-framework/sample-agent/docs/design.md @@ -12,22 +12,22 @@ This sample demonstrates a weather-focused agent built using the Microsoft Agent - Streaming responses to clients - Conversation thread management - Dual authentication (agentic and OBO handlers) -- Microsoft Agent 365 observability integration +- Auto-instrumentation via `Microsoft.OpenTelemetry` distro ## Architecture ``` ┌─────────────────────────────────────────────────────────────────┐ │ Program.cs │ -│ ┌─────────────┐ ┌─────────────┐ ┌──────────────────────────┐ │ -│ │ OpenTelemetry│ │ A365 Tracing│ │ ASP.NET Authentication │ │ -│ └─────────────┘ └─────────────┘ └──────────────────────────┘ │ +│ ┌──────────────────────┐ ┌──────────────────────────────────┐ │ +│ │ Microsoft.OpenTelemetry│ │ ASP.NET Authentication │ │ +│ └──────────────────────┘ └──────────────────────────────────┘ │ │ │ │ │ ┌─────────────────────────────────────────────────────────────┐│ │ │ Dependency Injection Container ││ -│ │ ┌─────────┐ ┌───────────┐ ┌──────────┐ ┌───────────────┐ ││ -│ │ │IChatClient│ │IMcpToolSvc│ │IStorage │ │ITokenCache │ ││ -│ │ └─────────┘ └───────────┘ └──────────┘ └───────────────┘ ││ +│ │ ┌─────────┐ ┌───────────┐ ┌──────────┐ ││ +│ │ │IChatClient│ │IMcpToolSvc│ │IStorage │ ││ +│ │ └─────────┘ └───────────┘ └──────────┘ ││ │ └─────────────────────────────────────────────────────────────┘│ └─────────────────────────────────────────────────────────────────┘ │ @@ -58,7 +58,7 @@ This sample demonstrates a weather-focused agent built using the Microsoft Agent ### Program.cs Entry point that configures: -- OpenTelemetry and Agent 365 tracing +- `Microsoft.OpenTelemetry` distro (`UseMicrosoftOpenTelemetry`) - MCP tool services (`IMcpToolRegistrationService`, `IMcpToolServerConfigurationService`) - Authentication middleware - IChatClient with Azure OpenAI @@ -79,36 +79,28 @@ Local tool implementation for weather queries: ### Tools/DateTimeFunctionTool.cs Utility tool for date/time queries. -### telemetry/AgentMetrics.cs -Custom observability helpers for tracing agent operations. - ## Message Flow ``` -1. HTTP POST /api/messages - │ -2. AgentMetrics.InvokeObservedHttpOperation() +1. HTTP POST /api/messages [auto-instrumented by ASP.NET Core] │ -3. adapter.ProcessAsync() → MyAgent.OnMessageAsync() +2. adapter.ProcessAsync() → MyAgent.OnMessageAsync() │ -4. A365OtelWrapper.InvokeObservedAgentOperation() - │ └── Observability context setup +3. Send ack + typing indicator to Teams │ -5. StreamingResponse.QueueInformativeUpdateAsync("Just a moment...") - │ -6. GetClientAgent() +4. GetClientAgent() │ ├── Create local tools (DateTime, Weather) │ ├── GetMcpToolsAsync() from MCP servers │ └── Build ChatClientAgent with instructions │ -7. GetConversationThread() - Load or create thread +5. GetConversationSessionAsync() - Load or create session │ -8. agent.RunStreamingAsync() +6. agent.RunStreamingAsync() [auto-instrumented: gen_ai.* on chat span] │ └── Stream responses to client │ -9. Save thread state +7. Save session state │ -10. StreamingResponse.EndStreamAsync() +8. StreamingResponse.EndStreamAsync() ``` ## Tool Integration @@ -169,25 +161,38 @@ var a365Tools = await toolService.GetMcpToolsAsync( ### Environment Variables ```bash ASPNETCORE_ENVIRONMENT=Development -BEARER_TOKEN=your-bearer-token # Development only -SKIP_TOOLING_ON_ERRORS=true # Development fallback +OTEL_SERVICE_NAME=Agent Framework Sample # Sets service.name in traces +BEARER_TOKEN=your-bearer-token # Development only +SKIP_TOOLING_ON_ERRORS=true # Development fallback ``` ## Observability -### Tracing Setup +Observability is provided entirely by the `Microsoft.OpenTelemetry` distro — no custom tracing code in this sample. + +### Setup ```csharp -builder.ConfigureOpenTelemetry(); -builder.Services.AddAgenticTracingExporter(clusterCategory: "production"); -builder.AddA365Tracing(config => { - config.WithAgentFramework(); +builder.UseMicrosoftOpenTelemetry(o => +{ + o.Exporters = builder.Environment.IsDevelopment() + ? ExportTarget.Agent365 | ExportTarget.Console + : ExportTarget.Agent365; + + o.Instrumentation.EnableAspNetCoreInstrumentation = true; + o.Instrumentation.EnableHttpClientInstrumentation = true; + o.Instrumentation.EnableAzureSdkInstrumentation = true; }); ``` -### Observed Operations -- `agent.process_message` - HTTP endpoint -- `MessageProcessor` - Message handling -- `WelcomeMessage` - Welcome flow +### Auto-instrumented Spans +- `POST /api/messages` — inbound request (ASP.NET Core) +- `POST login.microsoftonline.com` — MSAL token acquisition (HttpClient) +- `POST smba.trafficmanager.net` — outbound Teams messages (HttpClient) +- `POST …openai.azure.com/…/chat/completions` — Azure OpenAI HTTP call (HttpClient) +- `chat ` — `gen_ai.*` semantic attributes: model, tools, messages, tokens (Microsoft.Extensions.AI) +- `invoke_agent ` — agent-level span with agent ID and token counts (Microsoft.Agents.AI) + +The `gen_ai.*` attributes come from `.UseOpenTelemetry(sourceName: null, ...)` on the `ChatClientAgent` builder — the only explicit instrumentation call in the sample. ## Authentication @@ -238,8 +243,8 @@ turnState.Conversation.SetValue("conversation.threadInfo", ProtocolJsonSerialize ```xml - - + + ``` diff --git a/dotnet/agent-framework/sample-agent/telemetry/A365OtelWrapper.cs b/dotnet/agent-framework/sample-agent/telemetry/A365OtelWrapper.cs deleted file mode 100644 index fd408907..00000000 --- a/dotnet/agent-framework/sample-agent/telemetry/A365OtelWrapper.cs +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Microsoft.Agents.A365.Observability.Hosting.Caching; -using Microsoft.Agents.A365.Observability.Runtime.Common; -using Microsoft.Agents.A365.Runtime.Utils; -using Microsoft.Agents.Builder; -using Microsoft.Agents.Builder.App.UserAuth; -using Microsoft.Agents.Builder.State; - -namespace Agent365AgentFrameworkSampleAgent.telemetry -{ - public static class A365OtelWrapper - { - public static async Task InvokeObservedAgentOperation( - string operationName, - ITurnContext turnContext, - ITurnState turnState, - IExporterTokenCache? agentTokenCache, - UserAuthorization authSystem, - string authHandlerName, - ILogger? logger, - Func func - ) - { - // Wrap the operation with AgentSDK observability. - await AgentMetrics.InvokeObservedAgentOperation( - operationName, - turnContext, - async () => - { - // Resolve the tenant and agent id being used to communicate with A365 services. - (string agentId, string tenantId) = await ResolveTenantAndAgentId(turnContext, authSystem, authHandlerName); - - using var baggageScope = new BaggageBuilder() - .TenantId(tenantId) - .AgentId(agentId) - .Build(); - - try - { - agentTokenCache?.RegisterObservability(agentId, tenantId, new AgenticTokenStruct( - authSystem, - turnContext, - authHandlerName - ), EnvironmentUtils.GetObservabilityAuthenticationScope()); - } - catch (Exception ex) - { - logger?.LogWarning($"There was an error registering for observability: {ex.Message}"); - } - - // Invoke the actual operation. - await func().ConfigureAwait(false); - }).ConfigureAwait(false); - } - - /// - /// Resolve Tenant and Agent Id from the turn context. - /// - /// - /// - private static async Task<(string agentId, string tenantId)> ResolveTenantAndAgentId(ITurnContext turnContext, UserAuthorization authSystem, string authHandlerName) - { - string agentId = ""; - if (turnContext.Activity.IsAgenticRequest()) - { - agentId = turnContext.Activity.GetAgenticInstanceId(); - } - else - { - if (authSystem != null && !string.IsNullOrEmpty(authHandlerName)) - agentId = Utility.ResolveAgentIdentity(turnContext, await authSystem.GetTurnTokenAsync(turnContext, authHandlerName)); - } - agentId = agentId ?? Guid.Empty.ToString(); - string? tempTenantId = turnContext?.Activity?.Conversation?.TenantId ?? turnContext?.Activity?.Recipient?.TenantId; - string tenantId = tempTenantId ?? Guid.Empty.ToString(); - - return (agentId, tenantId); - } - - } -} diff --git a/dotnet/agent-framework/sample-agent/telemetry/AgentMetrics.cs b/dotnet/agent-framework/sample-agent/telemetry/AgentMetrics.cs deleted file mode 100644 index a43af9fb..00000000 --- a/dotnet/agent-framework/sample-agent/telemetry/AgentMetrics.cs +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Microsoft.Agents.Builder; -using Microsoft.Agents.Core; -using OpenTelemetry; -using OpenTelemetry.Metrics; -using OpenTelemetry.Resources; -using System; -using System.Diagnostics; -using System.Diagnostics.Metrics; - -namespace Agent365AgentFrameworkSampleAgent.telemetry -{ - public static class AgentMetrics - { - public static readonly string SourceName = "A365.AgentFramework"; - - public static readonly ActivitySource ActivitySource = new(SourceName); - - private static readonly Meter Meter = new ("A365.AgentFramework", "1.0.0"); - - public static readonly Counter MessageProcessedCounter = Meter.CreateCounter( - "agent.messages.processed", - "messages", - "Number of messages processed by the agent"); - - public static readonly Counter RouteExecutedCounter = Meter.CreateCounter( - "agent.routes.executed", - "routes", - "Number of routes executed by the agent"); - - public static readonly Histogram MessageProcessingDuration = Meter.CreateHistogram( - "agent.message.processing.duration", - "ms", - "Duration of message processing in milliseconds"); - - public static readonly Histogram RouteExecutionDuration = Meter.CreateHistogram( - "agent.route.execution.duration", - "ms", - "Duration of route execution in milliseconds"); - - public static readonly UpDownCounter ActiveConversations = Meter.CreateUpDownCounter( - "agent.conversations.active", - "conversations", - "Number of active conversations"); - - - public static Activity InitializeMessageHandlingActivity(string handlerName, ITurnContext context) - { - var activity = ActivitySource.StartActivity(handlerName); - activity?.SetTag("Activity.Type", context.Activity.Type.ToString()); - activity?.SetTag("Agent.IsAgentic", context.IsAgenticRequest()); - activity?.SetTag("Caller.Id", context.Activity.From?.Id); - activity?.SetTag("Conversation.Id", context.Activity.Conversation?.Id); - activity?.SetTag("Channel.Id", context.Activity.ChannelId?.ToString()); - activity?.SetTag("Message.Text.Length", context.Activity.Text?.Length ?? 0); - - activity?.AddEvent(new ActivityEvent("Message.Processed", DateTimeOffset.UtcNow, new() - { - ["Agent.IsAgentic"] = context.IsAgenticRequest(), - ["Caller.Id"] = context.Activity.From?.Id, - ["Channel.Id"] = context.Activity.ChannelId?.ToString(), - ["Message.Id"] = context.Activity.Id, - ["Message.Text"] = context.Activity.Text - })); - return activity!; - } - - public static void FinalizeMessageHandlingActivity(Activity activity, ITurnContext context, long duration, bool success) - { - MessageProcessingDuration.Record(duration, - new("Conversation.Id", context.Activity.Conversation?.Id ?? "unknown"), - new("Channel.Id", context.Activity.ChannelId?.ToString() ?? "unknown")); - - RouteExecutedCounter.Add(1, - new("Route.Type", "message_handler"), - new("Conversation.Id", context.Activity.Conversation?.Id ?? "unknown")); - - if (success) - { - activity?.SetStatus(ActivityStatusCode.Ok); - } - else - { - activity?.SetStatus(ActivityStatusCode.Error); - } - activity?.Stop(); - activity?.Dispose(); - } - - public static Task InvokeObservedHttpOperation(string operationName, Action func) - { - using var activity = ActivitySource.StartActivity(operationName); - try - { - func(); - activity?.SetStatus(ActivityStatusCode.Ok); - } - catch (Exception ex) - { - activity?.SetStatus(ActivityStatusCode.Error, ex.Message); - activity?.AddEvent(new ActivityEvent("exception", DateTimeOffset.UtcNow, new() - { - ["exception.type"] = ex.GetType().FullName, - ["exception.message"] = ex.Message, - ["exception.stacktrace"] = ex.StackTrace - })); - throw; - } - return Task.CompletedTask; - } - - public static Task InvokeObservedAgentOperation(string operationName, ITurnContext context, Func func) - { - MessageProcessedCounter.Add(1); - // Init the activity for observability - var activity = InitializeMessageHandlingActivity(operationName, context); - var routeStopwatch = Stopwatch.StartNew(); - try - { - return func(); - } - catch (Exception ex) - { - activity?.SetStatus(ActivityStatusCode.Error, ex.Message); - activity?.AddEvent(new ActivityEvent("exception", DateTimeOffset.UtcNow, new() - { - ["exception.type"] = ex.GetType().FullName, - ["exception.message"] = ex.Message, - ["exception.stacktrace"] = ex.StackTrace - })); - throw; - } - finally - { - routeStopwatch.Stop(); - FinalizeMessageHandlingActivity(activity, context, routeStopwatch.ElapsedMilliseconds, true); - } - } - } -} diff --git a/dotnet/semantic-kernel/sample-agent/Agents/MyAgent.cs b/dotnet/semantic-kernel/sample-agent/Agents/MyAgent.cs index 1dbaeab0..5a111a3c 100644 --- a/dotnet/semantic-kernel/sample-agent/Agents/MyAgent.cs +++ b/dotnet/semantic-kernel/sample-agent/Agents/MyAgent.cs @@ -5,7 +5,6 @@ using Agent365SemanticKernelSampleAgent.telemetry; using AgentNotification; using Microsoft.Agents.A365.Notifications.Models; -using Microsoft.Agents.A365.Observability.Caching; using Microsoft.Agents.A365.Tooling.Extensions.SemanticKernel.Services; using Microsoft.Agents.Builder; using Microsoft.Agents.Builder.App; @@ -27,7 +26,6 @@ public class MyAgent : AgentApplication { private readonly Kernel _kernel; private readonly IMcpToolRegistrationService _toolsService; - private readonly IExporterTokenCache _agentTokenCache; private readonly ILogger _logger; private readonly IConfiguration _configuration; // Setup reusable auto sign-in handlers @@ -38,12 +36,11 @@ public class MyAgent : AgentApplication internal static bool IsApplicationInstalled { get; set; } = false; internal static bool TermsAndConditionsAccepted { get; set; } = false; - public MyAgent(AgentApplicationOptions options, IConfiguration configuration, Kernel kernel, IMcpToolRegistrationService toolService, IExporterTokenCache agentTokenCache, ILogger logger) : base(options) + public MyAgent(AgentApplicationOptions options, IConfiguration configuration, Kernel kernel, IMcpToolRegistrationService toolService, ILogger logger) : base(options) { _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); _kernel = kernel ?? throw new ArgumentNullException(nameof(kernel)); _toolsService = toolService ?? throw new ArgumentNullException(nameof(toolService)); - _agentTokenCache = agentTokenCache ?? throw new ArgumentNullException(nameof(agentTokenCache)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); // Disable for development purpose. In production, you would typically want to have the user accept the terms and conditions on first use and then store that in a retrievable location. @@ -100,7 +97,6 @@ await A365OtelWrapper.InvokeObservedAgentOperation( "MessageProcessor", turnContext, turnState, - _agentTokenCache, UserAuthorization, ObservabilityAuthHandlerName, _logger, @@ -187,7 +183,6 @@ await A365OtelWrapper.InvokeObservedAgentOperation( "AgentNotificationActivityAsync", turnContext, turnState, - _agentTokenCache, UserAuthorization, ObservabilityAuthHandlerName, _logger, @@ -302,7 +297,6 @@ await A365OtelWrapper.InvokeObservedAgentOperation( "OnHireMessageAsync", turnContext, turnState, - _agentTokenCache, UserAuthorization, ObservabilityAuthHandlerName, _logger, diff --git a/dotnet/semantic-kernel/sample-agent/Program.cs b/dotnet/semantic-kernel/sample-agent/Program.cs index 35c0ccb4..c70100ef 100644 --- a/dotnet/semantic-kernel/sample-agent/Program.cs +++ b/dotnet/semantic-kernel/sample-agent/Program.cs @@ -3,9 +3,7 @@ using Agent365SemanticKernelSampleAgent.Agents; using Agent365SemanticKernelSampleAgent.telemetry; -using Microsoft.Agents.A365.Observability; -using Microsoft.Agents.A365.Observability.Extensions.SemanticKernel; -using Microsoft.Agents.A365.Observability.Runtime; +using Microsoft.OpenTelemetry; using Microsoft.Agents.A365.Tooling.Extensions.SemanticKernel.Services; using Microsoft.Agents.A365.Tooling.Services; using Microsoft.Agents.Builder; @@ -23,8 +21,19 @@ WebApplicationBuilder builder = WebApplication.CreateBuilder(args); -// Setup Aspire service defaults, including OpenTelemetry, Service Discovery, Resilience, and Health Checks - builder.ConfigureOpenTelemetry(); +// Configure OpenTelemetry distro — Console exporter only in Development to avoid PII leaks +builder.UseMicrosoftOpenTelemetry(o => +{ + o.Exporters = builder.Environment.IsDevelopment() + ? ExportTarget.Agent365 | ExportTarget.Console + : ExportTarget.Agent365; + + // Agent365-only export suppresses infrastructure instrumentation by default. + // Re-enable explicitly so HTTP calls (Azure OpenAI, auth, Teams) appear in traces. + o.Instrumentation.EnableAspNetCoreInstrumentation = true; + o.Instrumentation.EnableHttpClientInstrumentation = true; + o.Instrumentation.EnableAzureSdkInstrumentation = true; +}); if (builder.Environment.IsDevelopment()) { @@ -57,15 +66,6 @@ apiKey: builder.Configuration.GetSection("AIServices:OpenAI").GetValue("ApiKey")!); } -// Configure observability. -builder.Services.AddAgenticTracingExporter(); - -// Add A365 tracing with Semantic Kernel integration -builder.AddA365Tracing(config => -{ - config.WithSemanticKernel(); -}); - // Add AgentApplicationOptions from appsettings section "AgentApplication". builder.AddAgentApplicationOptions(); @@ -97,12 +97,9 @@ app.UseAuthorization(); // This receives incoming messages from Azure Bot Service or other SDK Agents -var incomingRoute = app.MapPost("/api/messages", async (HttpRequest request, HttpResponse response, IAgentHttpAdapter adapter, IAgent agent, CancellationToken cancellationToken) => +app.MapPost("/api/messages", async (HttpRequest request, HttpResponse response, IAgentHttpAdapter adapter, IAgent agent, CancellationToken cancellationToken) => { - await AgentMetrics.InvokeObservedHttpOperation("agent.process_message", async () => - { - await adapter.ProcessAsync(request, response, agent, cancellationToken); - }).ConfigureAwait(false); + await adapter.ProcessAsync(request, response, agent, cancellationToken); }); // Health check endpoint for CI/CD pipelines and monitoring diff --git a/dotnet/semantic-kernel/sample-agent/SemanticKernelSampleAgent.csproj b/dotnet/semantic-kernel/sample-agent/SemanticKernelSampleAgent.csproj index af011466..d4fd4645 100644 --- a/dotnet/semantic-kernel/sample-agent/SemanticKernelSampleAgent.csproj +++ b/dotnet/semantic-kernel/sample-agent/SemanticKernelSampleAgent.csproj @@ -22,24 +22,18 @@ - + - - - - - - + + + + + + - - - - - - - + diff --git a/dotnet/semantic-kernel/sample-agent/telemetry/A365OtelWrapper.cs b/dotnet/semantic-kernel/sample-agent/telemetry/A365OtelWrapper.cs index e716e942..048e4240 100644 --- a/dotnet/semantic-kernel/sample-agent/telemetry/A365OtelWrapper.cs +++ b/dotnet/semantic-kernel/sample-agent/telemetry/A365OtelWrapper.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using Microsoft.Agents.A365.Observability.Caching; using Microsoft.Agents.A365.Observability.Runtime.Common; using Microsoft.Agents.A365.Runtime.Utils; using Microsoft.Agents.Builder; @@ -19,7 +18,6 @@ public static async Task InvokeObservedAgentOperation( string operationName, ITurnContext turnContext, ITurnState turnState, - IExporterTokenCache? agentTokenCache, UserAuthorization authSystem, string authHandlerName, ILogger? logger, @@ -40,20 +38,6 @@ await AgentMetrics.InvokeObservedAgentOperation( .AgentId(agentId) .Build(); - try - { - agentTokenCache?.RegisterObservability(agentId, tenantId, new AgenticTokenStruct - { - UserAuthorization = authSystem, - TurnContext = turnContext, - AuthHandlerName = authHandlerName - }, EnvironmentUtils.GetObservabilityAuthenticationScope()); - } - catch (Exception ex) - { - logger?.LogWarning($"There was an error registering for observability: {ex.Message}"); - } - // Invoke the actual operation. await func().ConfigureAwait(false); }).ConfigureAwait(false); diff --git a/dotnet/semantic-kernel/sample-agent/telemetry/AgentOTELExtensions.cs b/dotnet/semantic-kernel/sample-agent/telemetry/AgentOTELExtensions.cs deleted file mode 100644 index 173067d5..00000000 --- a/dotnet/semantic-kernel/sample-agent/telemetry/AgentOTELExtensions.cs +++ /dev/null @@ -1,207 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Diagnostics.HealthChecks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Diagnostics.HealthChecks; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.ServiceDiscovery; -using OpenTelemetry; -using OpenTelemetry.Metrics; -using OpenTelemetry.Resources; -using OpenTelemetry.Trace; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Agent365SemanticKernelSampleAgent.telemetry -{ - // Adds common Aspire services: service discovery, resilience, health checks, and OpenTelemetry. - // This can be used by ASP.NET Core apps, Azure Functions, and other .NET apps using the Generic Host. - // This allows you to use the local aspire desktop and monitor Agents SDK operations. - // To learn more about using the local aspire desktop, see https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/dashboard/standalone?tabs=bash - public static class AgentOTELExtensions - { - private const string HealthEndpointPath = "/health"; - private const string AlivenessEndpointPath = "/alive"; - - public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder - { - builder.ConfigureOpenTelemetry(); - - builder.AddDefaultHealthChecks(); - - builder.Services.AddServiceDiscovery(); - - builder.Services.ConfigureHttpClientDefaults(http => - { - // Turn on resilience by default - http.AddStandardResilienceHandler(); - - // Turn on service discovery by default - http.AddServiceDiscovery(); - }); - - // Uncomment the following to restrict the allowed schemes for service discovery. - // builder.Services.Configure(options => - // { - // options.AllowedSchemes = ["https"]; - // }); - - return builder; - } - - public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder - { - builder.Logging.AddOpenTelemetry(logging => - { - logging.IncludeFormattedMessage = true; - logging.IncludeScopes = true; - }); - - builder.Services.AddOpenTelemetry() - .ConfigureResource(r => r - .Clear() - .AddService( - serviceName: "A365.SemanticKernel", - serviceVersion: "1.0.0", - serviceInstanceId: Environment.MachineName) - .AddAttributes(new Dictionary - { - ["deployment.environment"] = builder.Environment.EnvironmentName, - ["service.namespace"] = "Microsoft.Agents" - })) - .WithMetrics(metrics => - { - metrics.AddAspNetCoreInstrumentation() - .AddHttpClientInstrumentation() - .AddRuntimeInstrumentation() - .AddMeter("agent.messages.processed", - "agent.routes.executed", - "agent.conversations.active", - "agent.route.execution.duration", - "agent.message.processing.duration"); - }) - .WithTracing(tracing => - { - tracing.AddSource(builder.Environment.ApplicationName) - .AddSource( - "A365.SemanticKernel", - "A365.SemanticKernel.MyAgent", - "Microsoft.Agents.Builder", - "Microsoft.Agents.Hosting", - "Microsoft.AspNetCore", - "System.Net.Http" - ) - .AddAspNetCoreInstrumentation(tracing => - { - // Exclude health check requests from tracing - tracing.Filter = context => - !context.Request.Path.StartsWithSegments(HealthEndpointPath) - && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath); - tracing.RecordException = true; - tracing.EnrichWithHttpRequest = (activity, request) => - { - activity.SetTag("http.request.body.size", request.ContentLength); - activity.SetTag("user_agent", request.Headers.UserAgent); - }; - tracing.EnrichWithHttpResponse = (activity, response) => - { - activity.SetTag("http.response.body.size", response.ContentLength); - }; - }) - // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) - //.AddGrpcClientInstrumentation() - .AddHttpClientInstrumentation(o => - { - o.RecordException = true; - // Enrich outgoing request/response with extra tags - o.EnrichWithHttpRequestMessage = (activity, request) => - { - activity.SetTag("http.request.method", request.Method); - activity.SetTag("http.request.host", request.RequestUri?.Host); - activity.SetTag("http.request.useragent", request.Headers?.UserAgent); - }; - o.EnrichWithHttpResponseMessage = (activity, response) => - { - activity.SetTag("http.response.status_code", (int)response.StatusCode); - //activity.SetTag("http.response.headers", response.Content.Headers); - // Convert response.Content.Headers to a string array: "HeaderName=val1,val2" - var headerList = response.Content?.Headers? - .Select(h => $"{h.Key}={string.Join(",", h.Value)}") - .ToArray(); - - if (headerList is { Length: > 0 }) - { - // Set as an array tag (preferred for OTEL exporters supporting array-of-primitive attributes) - activity.SetTag("http.response.headers", headerList); - - // (Optional) Also emit individual header tags (comment out if too high-cardinality) - // foreach (var h in response.Content.Headers) - // { - // activity.SetTag($"http.response.header.{h.Key.ToLowerInvariant()}", string.Join(",", h.Value)); - // } - } - - }; - // Example filter: suppress telemetry for health checks - o.FilterHttpRequestMessage = request => - !request.RequestUri?.AbsolutePath.Contains("health", StringComparison.OrdinalIgnoreCase) ?? true; - }); - }); - - //builder.AddOpenTelemetryExporters(); - return builder; - } - - public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder - { - builder.Services.AddHealthChecks() - // Add a default liveness check to ensure app is responsive - .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); - - return builder; - } - - public static WebApplication MapDefaultEndpoints(this WebApplication app) - { - // Adding health checks endpoints to applications in non-development environments has security implications. - // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. - if (app.Environment.IsDevelopment()) - { - // All health checks must pass for app to be considered ready to accept traffic after starting - app.MapHealthChecks(HealthEndpointPath); - - // Only health checks tagged with the "live" tag must pass for app to be considered alive - app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions - { - Predicate = r => r.Tags.Contains("live") - }); - } - - return app; - } - - private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder - { - var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); - - if (useOtlpExporter) - { - builder.Services.AddOpenTelemetry().UseOtlpExporter(); - } - - // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) - //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) - //{ - // builder.Services.AddOpenTelemetry() - // .UseAzureMonitor(); - //} - - return builder; - } - - } -}