diff --git a/.github/workflows/e2e-orchestrator.yml b/.github/workflows/e2e-orchestrator.yml index ec6eeccd..769db745 100644 --- a/.github/workflows/e2e-orchestrator.yml +++ b/.github/workflows/e2e-orchestrator.yml @@ -22,6 +22,7 @@ on: options: - '' - 'python-openai' + - 'python-af' - 'nodejs-openai' - 'nodejs-langchain' - 'dotnet-sk' @@ -37,6 +38,12 @@ jobs: if: ${{ github.event.inputs.sample == '' || github.event.inputs.sample == 'python-openai' }} uses: ./.github/workflows/e2e-python-openai.yml secrets: inherit + + python-af: + name: Python Agent Framework E2E + if: ${{ github.event.inputs.sample == '' || github.event.inputs.sample == 'python-af' }} + uses: ./.github/workflows/e2e-python-agent-framework.yml + secrets: inherit nodejs-openai: name: Node.js OpenAI E2E @@ -49,7 +56,7 @@ jobs: if: ${{ github.event.inputs.sample == '' || github.event.inputs.sample == 'nodejs-langchain' }} uses: ./.github/workflows/e2e-nodejs-langchain.yml secrets: inherit - + dotnet-sk: name: .NET Semantic Kernel E2E if: ${{ github.event.inputs.sample == '' || github.event.inputs.sample == 'dotnet-sk' }} @@ -69,7 +76,7 @@ jobs: e2e-status: name: E2E Status runs-on: ubuntu-latest - needs: [python-openai, nodejs-openai, nodejs-langchain, dotnet-sk, dotnet-af] + needs: [python-openai, python-af, nodejs-openai, nodejs-langchain, dotnet-sk, dotnet-af] if: always() steps: @@ -78,7 +85,7 @@ jobs: echo "Checking E2E test results..." # Get all job results (skipped jobs are fine, failed jobs are not) - results="${{ needs.python-openai.result }} ${{ needs.nodejs-openai.result }} ${{ needs.nodejs-langchain.result }} ${{ needs.dotnet-sk.result }} ${{ needs.dotnet-af.result }}" + results="${{ needs.python-openai.result }} ${{ needs.python-af.result }} ${{ needs.nodejs-openai.result }} ${{ needs.nodejs-langchain.result }} ${{ needs.dotnet-sk.result }} ${{ needs.dotnet-af.result }}" echo "Job results: $results" @@ -97,6 +104,7 @@ jobs: # Determine overall status OVERALL_STATUS="✅ All Tests Passed" if [[ "${{ needs.python-openai.result }}" != "success" ]] || \ + [[ "${{ needs.python-af.result }}" != "success" ]] || \ [[ "${{ needs.nodejs-openai.result }}" != "success" ]] || \ [[ "${{ needs.nodejs-langchain.result }}" != "success" ]] || \ [[ "${{ needs.dotnet-sk.result }}" != "success" ]] || \ @@ -106,6 +114,7 @@ jobs: # Generate status icons using if-else for reliability if [[ "${{ needs.python-openai.result }}" == "success" ]]; then PYTHON_OPENAI_ICON="✅"; else PYTHON_OPENAI_ICON="❌"; fi + if [[ "${{ needs.python-af.result }}" == "success" ]]; then PYTHON_AF_ICON="✅"; else PYTHON_AF_ICON="❌"; fi if [[ "${{ needs.nodejs-openai.result }}" == "success" ]]; then NODEJS_OPENAI_ICON="✅"; else NODEJS_OPENAI_ICON="❌"; fi if [[ "${{ needs.nodejs-langchain.result }}" == "success" ]]; then NODEJS_LANGCHAIN_ICON="✅"; else NODEJS_LANGCHAIN_ICON="❌"; fi if [[ "${{ needs.dotnet-sk.result }}" == "success" ]]; then DOTNET_SK_ICON="✅"; else DOTNET_SK_ICON="❌"; fi @@ -118,6 +127,7 @@ jobs: echo "| Sample | Status | Result |" >> $GITHUB_STEP_SUMMARY echo "|--------|--------|--------|" >> $GITHUB_STEP_SUMMARY echo "| Python OpenAI | $PYTHON_OPENAI_ICON | ${{ needs.python-openai.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Python Agent Framework | $PYTHON_AF_ICON | ${{ needs.python-af.result }} |" >> $GITHUB_STEP_SUMMARY echo "| Node.js OpenAI | $NODEJS_OPENAI_ICON | ${{ needs.nodejs-openai.result }} |" >> $GITHUB_STEP_SUMMARY echo "| Node.js LangChain | $NODEJS_LANGCHAIN_ICON | ${{ needs.nodejs-langchain.result }} |" >> $GITHUB_STEP_SUMMARY echo "| .NET Semantic Kernel | $DOTNET_SK_ICON | ${{ needs.dotnet-sk.result }} |" >> $GITHUB_STEP_SUMMARY @@ -126,17 +136,19 @@ jobs: echo "> 📦 **SDK Versions**: Click on each sample workflow above to view detailed SDK version information in the step summary." >> $GITHUB_STEP_SUMMARY - name: Post PR Comment - if: github.event_name == 'pull_request' + if: always() && github.event_name == 'pull_request' && github.event.pull_request.number uses: actions/github-script@v7 with: script: | const pythonOpenaiIcon = '${{ needs.python-openai.result }}' === 'success' ? '✅' : '❌'; + const pythonAfIcon = '${{ needs.python-af.result }}' === 'success' ? '✅' : '❌'; const nodejsOpenaiIcon = '${{ needs.nodejs-openai.result }}' === 'success' ? '✅' : '❌'; const nodejsLangchainIcon = '${{ needs.nodejs-langchain.result }}' === 'success' ? '✅' : '❌'; const dotnetSkIcon = '${{ needs.dotnet-sk.result }}' === 'success' ? '✅' : '❌'; const dotnetAfIcon = '${{ needs.dotnet-af.result }}' === 'success' ? '✅' : '❌'; const allPassed = '${{ needs.python-openai.result }}' === 'success' && + '${{ needs.python-af.result }}' === 'success' && '${{ needs.nodejs-openai.result }}' === 'success' && '${{ needs.nodejs-langchain.result }}' === 'success' && '${{ needs.dotnet-sk.result }}' === 'success' && @@ -150,6 +162,7 @@ jobs: '| Sample | Status | Result |', '|--------|--------|--------|', `| Python OpenAI | ${pythonOpenaiIcon} | ${{ needs.python-openai.result }} |`, + `| Python Agent Framework | ${pythonAfIcon} | ${{ needs.python-af.result }} |`, `| Node.js OpenAI | ${nodejsOpenaiIcon} | ${{ needs.nodejs-openai.result }} |`, `| Node.js LangChain | ${nodejsLangchainIcon} | ${{ needs.nodejs-langchain.result }} |`, `| .NET Semantic Kernel | ${dotnetSkIcon} | ${{ needs.dotnet-sk.result }} |`, @@ -160,14 +173,18 @@ jobs: `[View full test details](${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID})` ].join('\n'); + // Get PR number from event context + const prNumber = ${{ github.event.pull_request.number }}; + console.log(`PR number: ${prNumber}`); + // Find existing comment const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, - issue_number: context.issue.number, + issue_number: prNumber, }); - console.log(`Found ${comments.length} comments on PR #${context.issue.number}`); + console.log(`Found ${comments.length} comments on PR #${prNumber}`); const botComment = comments.find(comment => comment.user.login === 'github-actions[bot]' && @@ -192,7 +209,7 @@ jobs: const result = await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, - issue_number: context.issue.number, + issue_number: prNumber, body: body }); console.log(`Create result: ${result.status}, comment ID: ${result.data.id}`); diff --git a/.github/workflows/e2e-python-agent-framework.yml b/.github/workflows/e2e-python-agent-framework.yml new file mode 100644 index 00000000..a4f3c05f --- /dev/null +++ b/.github/workflows/e2e-python-agent-framework.yml @@ -0,0 +1,192 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +name: E2E - Python Agent Framework + +on: + workflow_call: # Allow orchestrator to call this workflow + workflow_dispatch: # Allow manual triggering + push: + branches: [main] + paths: + - 'python/agent-framework/**' + - 'scripts/e2e/**' + - '.github/workflows/e2e-python-agent-framework.yml' + pull_request: + branches: [main] + paths: + - 'python/agent-framework/**' + - 'scripts/e2e/**' + +permissions: + contents: read + +env: + SAMPLE_PATH: python/agent-framework/sample-agent + AGENT_PORT: 3979 + SCRIPTS_PATH: scripts/e2e + E2E_TESTS_PATH: tests/e2e + +jobs: + python-agent-framework: + name: Python Agent Framework Agent + runs-on: windows-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install uv package manager + run: pip install uv + + - name: Install dependencies + working-directory: ${{ env.SAMPLE_PATH }} + run: uv sync + + - name: Log SDK Versions + shell: pwsh + run: | + & "${{ env.SCRIPTS_PATH }}/Get-SDKVersions.ps1" ` + -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/README.md b/README.md index e7ca89f6..ad91f096 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ This repository contains sample agents and prompts for building with the Microso | Sample | Status | |--------|--------| | Python OpenAI | [![Python OpenAI](https://img.shields.io/github/actions/workflow/status/microsoft/Agent365-Samples/e2e-python-openai.yml?branch=main&label=E2E)](https://github.com/microsoft/Agent365-Samples/actions/workflows/e2e-python-openai.yml) | +| Python Agent Framework | [![Python AF](https://img.shields.io/github/actions/workflow/status/microsoft/Agent365-Samples/e2e-python-agent-framework.yml?branch=main&label=E2E)](https://github.com/microsoft/Agent365-Samples/actions/workflows/e2e-python-agent-framework.yml) | +| Python Google ADK | [![Python Google ADK](https://img.shields.io/github/actions/workflow/status/microsoft/Agent365-Samples/e2e-python-google-adk.yml?branch=main&label=E2E)](https://github.com/microsoft/Agent365-Samples/actions/workflows/e2e-python-google-adk.yml) | | Node.js OpenAI | [![Node.js OpenAI](https://img.shields.io/github/actions/workflow/status/microsoft/Agent365-Samples/e2e-nodejs-openai.yml?branch=main&label=E2E)](https://github.com/microsoft/Agent365-Samples/actions/workflows/e2e-nodejs-openai.yml) | | Node.js LangChain | [![Node.js LangChain](https://img.shields.io/github/actions/workflow/status/microsoft/Agent365-Samples/e2e-nodejs-langchain.yml?branch=main&label=E2E)](https://github.com/microsoft/Agent365-Samples/actions/workflows/e2e-nodejs-langchain.yml) | | .NET Semantic Kernel | [![.NET SK](https://img.shields.io/github/actions/workflow/status/microsoft/Agent365-Samples/e2e-dotnet-semantic-kernel.yml?branch=main&label=E2E)](https://github.com/microsoft/Agent365-Samples/actions/workflows/e2e-dotnet-semantic-kernel.yml) | diff --git a/nodejs/copilot-studio/sample-agent/src/index.ts b/nodejs/copilot-studio/sample-agent/src/index.ts index c5ca76ea..3e93cde7 100644 --- a/nodejs/copilot-studio/sample-agent/src/index.ts +++ b/nodejs/copilot-studio/sample-agent/src/index.ts @@ -16,6 +16,12 @@ const authConfig: AuthConfiguration = isProduction ? loadAuthConfigFromEnv() : { const server: Express = express() server.use(express.json()) + +// Health endpoint - before auth middleware so it's always accessible +server.get('/api/health', (_req, res: Response) => { + res.status(200).json({ status: 'healthy', timestamp: new Date().toISOString() }); +}); + server.use(authorizeJWT(authConfig)) server.post('/api/messages', (req: Request, res: Response) => { diff --git a/scripts/e2e/Start-Agent.ps1 b/scripts/e2e/Start-Agent.ps1 index d0af3863..1f7d575e 100644 --- a/scripts/e2e/Start-Agent.ps1 +++ b/scripts/e2e/Start-Agent.ps1 @@ -172,6 +172,12 @@ $scriptLines = @( # Add runtime-specific pre-flight checks if ($Runtime -eq "python") { + # Extract the Python module name from the start command (e.g., "uv run python main.py" -> "main") + $pythonModule = "main" + if ($StartCommand -match "python\s+(\w+)\.py") { + $pythonModule = $matches[1] + } + $scriptLines += @( "Write-Host 'Checking Python environment...'" "uv run python --version" @@ -179,9 +185,9 @@ if ($Runtime -eq "python") { "uv run pip list | Select-Object -First 20" "Write-Host ''" "Write-Host 'Testing Python can import main script...'" - "uv run python -c `"import start_with_generic_host; print('Import OK')`"" + "uv run python -c `"import $pythonModule; print('Import OK')`"" "if (`$LASTEXITCODE -ne 0) {" - " Write-Host 'ERROR: Failed to import start_with_generic_host.py' -ForegroundColor Red" + " Write-Host 'ERROR: Failed to import $pythonModule.py' -ForegroundColor Red" " exit 1" "}" )