Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 24 additions & 7 deletions .github/workflows/e2e-orchestrator.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ on:
options:
- ''
- 'python-openai'
- 'python-af'
- 'nodejs-openai'
- 'nodejs-langchain'
- 'dotnet-sk'
Expand All @@ -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
Expand All @@ -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' }}
Expand All @@ -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:
Expand All @@ -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"

Expand All @@ -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" ]] || \
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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' &&
Expand All @@ -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 }} |`,
Expand All @@ -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]' &&
Expand All @@ -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}`);
Expand Down
192 changes: 192 additions & 0 deletions .github/workflows/e2e-python-agent-framework.yml
Original file line number Diff line number Diff line change
@@ -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 }}
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
6 changes: 6 additions & 0 deletions nodejs/copilot-studio/sample-agent/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
10 changes: 8 additions & 2 deletions scripts/e2e/Start-Agent.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -172,16 +172,22 @@ $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"
"Write-Host 'Python packages:'"
"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"
"}"
)
Expand Down
Loading