Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 25 additions & 1 deletion .config/samples-config-v3.json
Original file line number Diff line number Diff line change
Expand Up @@ -937,6 +937,30 @@
"thumbnailPath": "images/screen009.jpg",
"gifPath": "images/screen009.jpg",
"suggested": true
},
{
"id": "data-analyst-agent-v2",
"shortId": "data-analyst-v2",
"onboardDate": "2025-12-23",
"title": "Data Analyst Agent v2",
"shortDescription": "Natural language interface for data exploration and visualization.",
"fullDescription": "This sample demonstrates how to build an AI-powered data analyst agent using Microsoft Teams SDK that can be integrated into Microsoft Teams. It helps users explore and visualize data through natural language conversations and Adaptive Cards charts.",
"types": [
"Custom Engine Agent"
],
"tags": [
"TS",
"Custom Engine Agent",
"Data Visualization",
"Adaptive Cards",
"LLM SQL",
"Microsoft Teams SDK"
],
"time": "10 mins to run",
"configuration": "Manual configurations required",
"thumbnailPath": "assets/demo.gif",
"gifPath": "assets/demo.gif",
"suggested": false
}
]
}
}
61 changes: 61 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Pull Request

## Description

<!-- Please provide a brief description of the changes in this PR -->

## Type of Change

- [ ] New sample onboarding (internal - source code in this repo)
- [ ] New sample onboarding (external - source code in another repo)
- [ ] Sample update/fix
- [ ] Documentation update
- [ ] Validation tool update
- [ ] Other (please describe)

---

## For New Sample Onboarding

### Checklist

- [ ] I have added/updated the sample entry in `.config/samples-config-v3.json`
- [ ] I have included a README.md with setup instructions
- [ ] I have included a thumbnail image with correct aspect ratio (40:23, e.g., 1600×920 or 800×460)

### Validation Results (Required)

> **Important**: You must run the validation tool locally and provide a screenshot of the results.

#### For Internal Samples (source code in this repo)

```bash
cd validation-tool
npm install
node validator.cjs -p ../<your-sample-folder>
```

#### For External Samples (source code in another repo)

```bash
cd validation-tool
npm install

# Clone your sample repo (sparse checkout recommended)
git clone --filter=blob:none --sparse <your-repo-url>
cd <repo-name>
git sparse-checkout set <path-to-sample>
cd ..

# Run validation
node validate-external.js <sample-id> ./<repo-name>
```


---

## Related Issues

<!-- Link any related issues here, e.g., "Fixes #123" or "Related to #456" -->
if any question related to validation, may refer to the [Sample Validation Guide](validation-tool/sample_validation.md)
if still has questions, may open a issue :)
69 changes: 69 additions & 0 deletions .github/prompts/onboard-sample.prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
mode: agent
description: "Onboard a sample to Microsoft 365 Agents Toolkit Samples repository"
tools: ["editFiles", "runInTerminal", "readFile"]
---

You are onboarding a sample to the Microsoft 365 Agents Toolkit Samples repository.

**CRITICAL: You must DO the work, not explain what to do. Execute each step by reading/writing files and running commands directly. Only ask the user when you truly cannot infer required information.**

The user will provide a sample folder name (e.g., `data-analyst-agent-v2`).

---

## Step 1: Locate and Analyze

1. Find `microsoft-365-agents-toolkit-samples` in workspace
2. Read sample's `package.json` (TS/JS), `.sln` (C#), or determine it's Python
3. Check if sample is internal (inside repo) or external

## Step 2: Fix m365agents.yml

1. Read the file (root for TS/JS, or `M365Agent/` for C#)
2. **DELETE** any `projectId` field if present
3. **ENSURE** this exists (add if missing):
```yaml
additionalMetadata:
sampleTag: TeamsFx-Samples:{folder-name}
```
4. **SAVE the file** with your changes

## Step 3: Fix manifest.json

1. Read `appPackage/manifest.json` (or `M365Agent/appPackage/manifest.json` for C#)
2. **ENSURE** id field is: `"id": "${{TEAMS_APP_ID}}"`
3. **SAVE the file** if changed

## Step 4: Update samples-config-v3.json

1. Read `.config/samples-config-v3.json`
2. Search for entry with matching `id`
3. If NOT found, **CREATE and INSERT** a new entry:
- Infer `title`, `shortDescription`, `fullDescription` from README.md
- Infer `types` and `tags` from project content
- Set `onboardDate` to today (YYYY-MM-DD)
- Set `thumbnailPath` and `gifPath` based on assets folder
- For external samples: get `downloadUrlInfo` via `git remote get-url origin`
4. **SAVE the file** with valid JSON

## Step 5: Run Validation (Internal TS/JS or C# only)

1. Run in terminal:
```bash
cd validation-tool && npm install && node validator.cjs -p ../{sample-folder}
```
2. If validation fails:
- **FIX the issues yourself** (resize images, update configs, etc.)
- Re-run validation until it passes
3. Only ask user if you cannot fix an issue automatically

## Step 6: Report Results

Show a brief checklist:
- [x] or [ ] m365agents.yml has sampleTag, no projectId
- [x] or [ ] manifest.json uses ${{TEAMS_APP_ID}}
- [x] or [ ] Entry in samples-config-v3.json
- [x] or [ ] Validation passed

List any issues that need user action.
19 changes: 18 additions & 1 deletion data-analyst-agent-v2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ The Data Analyst Agent v2 transforms how teams interact with data by providing a
2. Configure environment variables:
- Open the project in Visual Studio Code
- The Microsoft 365 Agents Toolkit will automatically generate the required environment files
- Update the `.localConfigs` file (or `.localConfigs.playground` if debug with playground) with your Azure OpenAI configuration:
- Update the `.env.local.user` file (or `.env.playground.user` if debug with playground) with your Azure OpenAI configuration:
- `SECRET_AZURE_OPENAI_API_KEY`: Your Azure OpenAI API key
- `AZURE_OPENAI_ENDPOINT`: Your Azure OpenAI endpoint
- `AZURE_OPENAI_DEPLOYMENT_NAME`: Your Azure OpenAI deployment name (e.g., gpt-4o)
Expand Down Expand Up @@ -152,6 +152,23 @@ Both evaluations will provide detailed feedback including:
| Date | Author | Comments |
| ------------ | ---------- | -------------------------------------- |
| Oct 31, 2025 | qinzhouxu | Onboard sample with Microsoft Teams SDK |
| Dec 16, 2025 | quke | fix issue and onboard sample |

## Known Issues

### Service Principal Creation Failure During Local Debug (Admin Accounts Only)

**Impact**: Local debug only. This does not affect debugging in Microsoft 365 Agents Playground or remote deployment to Azure.

**Symptom**: When running local debug with an M365 admin account, you may encounter the following error:

```
The client application <client-id> is missing service principal in the tenant <tenant-id>.
```

**Root Cause**: Teams Developer Portal has a known issue where it cannot automatically create a service principal for M365 admin accounts.

**Solution**: Use a non-admin M365 account for local debugging. You can create a regular user account in your M365 tenant and use it for the local debug workflow.

## Feedback
We really appreciate your feedback! If you encounter any issue or error, please report issues to us following the [Supporting Guide](https://github.com/OfficeDev/TeamsFx-Samples/blob/dev/SUPPORT.md). Meanwhile you can make [recording](https://aka.ms/teamsfx-record) of your journey with our product, they really make the product better. Thank you!
Binary file modified data-analyst-agent-v2/appPackage/outline.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified data-analyst-agent-v2/assets/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 28 additions & 4 deletions data-analyst-agent-v2/evals/judge/ac.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ consider the submission correct even if titles, labels, colors, or other propert
const prompt = new ChatPrompt({
instructions: systemMessage,
model: new OpenAIChatModel({
model: process.env.AOAI_MODEL!,
apiKey: process.env.AOAI_API_KEY!,
endpoint: process.env.AOAI_ENDPOINT!,
model: process.env.AZURE_OPENAI_DEPLOYMENT_NAME!,
apiKey: process.env.AZURE_OPENAI_API_KEY!,
endpoint: process.env.AZURE_OPENAI_ENDPOINT!,
apiVersion: '2025-04-01-preview',
}),
}).function(
Expand Down Expand Up @@ -90,7 +90,31 @@ consider the submission correct even if titles, labels, colors, or other propert
************
[END DATA]`;
const res = await prompt.send(userPrompt, { autoFunctionCalling: false });
const functionCallArgs = res.function_calls?.[0].arguments;

// Parse function call from content (model returns JSON in content when autoFunctionCalling is false)
let functionCallArgs: { result?: boolean; reasoning?: string } | undefined;

if (res.function_calls?.[0]?.arguments) {
functionCallArgs = res.function_calls[0].arguments;
} else if (res.content) {
// Try to parse JSON-like content without using a complex, backtracking-prone regex
try {
const resultMatch = res.content.match(/"result"\s*:\s*(true|false)/);
const reasoningMatch = res.content.match(/"reasoning"\s*:\s*"((?:[^"\\]|\\.)*)"/);

if (resultMatch && reasoningMatch) {
const rawReasoning = reasoningMatch[1];
functionCallArgs = {
result: resultMatch[1] === 'true',
reasoning: rawReasoning
.replace(/\\"/g, '"')
.replace(/\\n/g, '\n')
};
}
} catch (e) {
// Parsing failed, will use fallback
}
}

return {
result: functionCallArgs?.result || false,
Expand Down
46 changes: 42 additions & 4 deletions data-analyst-agent-v2/evals/judge/sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ The submitted answer may either be correct or incorrect. Determine which case ap
const prompt = new ChatPrompt({
instructions: systemMessage,
model: new OpenAIChatModel({
model: process.env.AOAI_MODEL!,
apiKey: process.env.AOAI_API_KEY!,
endpoint: process.env.AOAI_ENDPOINT!,
model: process.env.AZURE_OPENAI_DEPLOYMENT_NAME!,
apiKey: process.env.AZURE_OPENAI_API_KEY!,
endpoint: process.env.AZURE_OPENAI_ENDPOINT!,
apiVersion: '2025-04-01-preview',
}),
}).function(
Expand Down Expand Up @@ -73,7 +73,45 @@ The submitted answer may either be correct or incorrect. Determine which case ap
************
[END DATA]`;
const res = await prompt.send(userPrompt, { autoFunctionCalling: false });
const functionCallArgs = res.function_calls?.[0]?.arguments;

// Parse function call from content (model returns JSON in content when autoFunctionCalling is false)
let functionCallArgs: { result?: boolean; reasoning?: string } | undefined;

if (res.function_calls?.[0]?.arguments) {
functionCallArgs = res.function_calls[0].arguments;
} else if (res.content) {
// Try to parse JSON from content
try {
const content = res.content;

// First, try to extract a JSON object and parse it directly
const objectMatch = content.match(/\{[\s\S]*\}/);
if (objectMatch) {
const parsed = JSON.parse(objectMatch[0]);
if (typeof parsed.result === 'boolean' && typeof parsed.reasoning === 'string') {
functionCallArgs = {
result: parsed.result,
reasoning: parsed.reasoning
};
}
}

// Fallback: use a simpler regex extraction if JSON.parse did not yield result
if (!functionCallArgs) {
const fallbackMatch = content.match(
/"result"\s*:\s*(true|false)[\s\S]*?"reasoning"\s*:\s*"([^"\\]*(?:\\.[^"\\]*)*)"/
);
if (fallbackMatch) {
functionCallArgs = {
result: fallbackMatch[1] === 'true',
reasoning: fallbackMatch[2].replace(/\\"/g, '"').replace(/\\n/g, '\n')
};
}
}
} catch (e) {
// Parsing failed, will use fallback
}
}

return {
result: functionCallArgs?.result || false,
Expand Down
61 changes: 60 additions & 1 deletion data-analyst-agent-v2/evals/skills/adaptive-card-generation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ACJudge } from '../judge/ac';
import { AgentEvaluator } from './base-evaluator';
import { generateChartCard } from '../../src/cards';

new AgentEvaluator({
evalName: 'ac-eval',
Expand All @@ -8,7 +9,65 @@ new AgentEvaluator({
judge: ACJudge,
generatePrompt: (tc) =>
`Create an appropriate visualization for this data: ${JSON.stringify(tc.input_data)}. Please return a single card.\nUse the following type of visualization: ${tc.visualization_type}.`,
extractGenerated: (agent) => JSON.stringify(agent.attachments?.[0] ?? {}),
extractGenerated: (agent, response) => {
// First check if there are actual attachments from function execution
if (agent.attachments?.length > 0) {
const attachment = agent.attachments[0];
return JSON.stringify(attachment.content);
}

// When model returns JSON in content instead of calling functions
const content = response.content || '';

try {
// Try format 1: {"name":"functions.generate_card","arguments":{...}}
let startIdx = content.indexOf('{"name":"functions.generate_card"');
if (startIdx !== -1) {
let braceCount = 0;
let endIdx = startIdx;
for (let i = startIdx; i < content.length; i++) {
if (content[i] === '{') braceCount++;
else if (content[i] === '}') braceCount--;
if (braceCount === 0) {
endIdx = i + 1;
break;
}
}
const jsonStr = content.substring(startIdx, endIdx);
const functionCall = JSON.parse(jsonStr);
const { chartType, rows, options } = functionCall.arguments;
if (chartType && rows) {
const attachment = generateChartCard(chartType, rows, options);
return JSON.stringify(attachment.content);
}
}

// Try format 2: Direct {"chartType":"...","rows":[...],...} at start of content
startIdx = content.indexOf('{"chartType"');
if (startIdx !== -1) {
let braceCount = 0;
let endIdx = startIdx;
for (let i = startIdx; i < content.length; i++) {
if (content[i] === '{') braceCount++;
else if (content[i] === '}') braceCount--;
if (braceCount === 0) {
endIdx = i + 1;
break;
}
}
const jsonStr = content.substring(startIdx, endIdx);
const { chartType, rows, options } = JSON.parse(jsonStr);
if (chartType && rows) {
const attachment = generateChartCard(chartType, rows, options);
return JSON.stringify(attachment.content);
}
}
} catch (e) {
// Silent fail
}

return '{}';
},
extractExpected: (tc) => JSON.stringify(tc.expected_card),
extractInput: (tc) => JSON.stringify(tc.input_data),
}).run(process.argv.includes('--run-one'));
Loading
Loading