Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
}
]
}
}
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.
27 changes: 23 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 @@
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,26 @@
************
[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 jsonMatch = res.content.match(/\{[\s\S]*"result"\s*:\s*(true|false)[\s\S]*"reasoning"\s*:\s*"([^"]+(?:\\.[^"]*)*)"[\s\S]*\}/);
if (jsonMatch) {
functionCallArgs = {
result: jsonMatch[1] === 'true',
reasoning: jsonMatch[2].replace(/\\"/g, '"').replace(/\\n/g, '\n')
};
}
} catch (e) {
// Parsing failed, will use fallback
}
}

return {
result: functionCallArgs?.result || false,
Expand Down
27 changes: 23 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 @@
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,26 @@
************
[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 jsonMatch = res.content.match(/\{[\s\S]*"result"\s*:\s*(true|false)[\s\S]*"reasoning"\s*:\s*"([^"]+(?:\\.[^"]*)*)"[\s\S]*\}/);
if (jsonMatch) {
functionCallArgs = {
result: jsonMatch[1] === 'true',
reasoning: jsonMatch[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'));
16 changes: 13 additions & 3 deletions data-analyst-agent-v2/evals/skills/sql-generation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,19 @@
fileName: 'sql-eval.jsonl',
autoFunctionCalling: false,
judge: SQLJudge,
generatePrompt: (tc) => `Here's the user query: ${tc.user_query}. Let the SQL Prompt generate a SQL query based on the user's input and execute it.`,
extractGenerated: (_, response) =>
response.function_calls?.[0].arguments.text || 'MISSING_SQL_OUTPUT',
generatePrompt: (tc) => `Here's the user query: ${tc.user_query}. Generate a SQL query to answer this question.`,
extractGenerated: (_, response) => {
// When autoFunctionCalling is false, the model returns function call as JSON in content
const content = response.content || '';
// Try to extract JSON from content
const jsonMatch = content.match(/\{"name":\s*"functions\.execute_sql".*?"query"\s*:\s*"([^"]+(?:\\.[^"]*)*)".*?\}/s);
if (jsonMatch) {
// Unescape the query string
return jsonMatch[1].replace(/\\n/g, '\n').replace(/\\"/g, '"');
}
// Fallback: try to parse as function_calls array
return response.function_calls?.[0]?.arguments?.query || 'MISSING_SQL_OUTPUT';
},
extractExpected: (tc) => tc.sql_query,
extractInput: (tc) => tc.user_query,
}).run(process.argv.includes('--run-one'));
Loading
Loading