From ed62fe631dc44695a416e64b8fe6fba20e1e2463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien?= Date: Mon, 22 Sep 2025 20:01:54 +0000 Subject: [PATCH 01/24] fix: update devDependencies to latest versions in package.json.tpl --- .../common/declarative-agent-typespec/package.json.tpl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/templates/vsc/common/declarative-agent-typespec/package.json.tpl b/templates/vsc/common/declarative-agent-typespec/package.json.tpl index 0f39dcf0df7..dcfb996853d 100644 --- a/templates/vsc/common/declarative-agent-typespec/package.json.tpl +++ b/templates/vsc/common/declarative-agent-typespec/package.json.tpl @@ -2,10 +2,10 @@ "name": "{{SafeProjectNameLowerCase}}", "version": "1.0.0", "devDependencies": { - "@microsoft/typespec-m365-copilot": "1.0.0-rc.3", - "@typespec/compiler": "1.0.0-rc.1", - "@typespec/http": "1.0.0-rc.1", - "@typespec/openapi": "1.0.0-rc.1", - "@typespec/openapi3": "1.0.0-rc.1" + "@microsoft/typespec-m365-copilot": "1.0.0-rc.4", + "@typespec/compiler": "^1.0.0", + "@typespec/http": "^1.0.0", + "@typespec/openapi": "^1.0.0", + "@typespec/openapi3": "^1.0.0" } } \ No newline at end of file From 1b3df7fc311c00a3668c56fa5c1e9da105eb841c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien?= Date: Wed, 24 Sep 2025 14:44:45 +0000 Subject: [PATCH 02/24] feat: add script to generate TypeSpec variables from environment files --- .../declarative-agent-typespec/.gitignore.tpl | 4 +- .../declarative-agent-typespec/actions.tsp | 2 +- .../env/.env.dev.tpl | 1 + .../m365agents.yml.tpl | 7 ++ .../declarative-agent-typespec/main.tsp.tpl | 11 +-- .../package.json.tpl | 3 + .../scripts/generate-variable.js | 81 +++++++++++++++++++ 7 files changed, 98 insertions(+), 11 deletions(-) create mode 100644 templates/vsc/common/declarative-agent-typespec/scripts/generate-variable.js diff --git a/templates/vsc/common/declarative-agent-typespec/.gitignore.tpl b/templates/vsc/common/declarative-agent-typespec/.gitignore.tpl index 4428b74c76d..5d7cd10226a 100644 --- a/templates/vsc/common/declarative-agent-typespec/.gitignore.tpl +++ b/templates/vsc/common/declarative-agent-typespec/.gitignore.tpl @@ -13,4 +13,6 @@ node_modules/ .DS_Store # generated files -appPackage/.generated \ No newline at end of file +appPackage/.generated +# generated variables +variables.tsp \ No newline at end of file diff --git a/templates/vsc/common/declarative-agent-typespec/actions.tsp b/templates/vsc/common/declarative-agent-typespec/actions.tsp index 3fb25acf084..1945766b58f 100644 --- a/templates/vsc/common/declarative-agent-typespec/actions.tsp +++ b/templates/vsc/common/declarative-agent-typespec/actions.tsp @@ -22,7 +22,7 @@ namespace GitHubAPI { /** * The base URL for the GitHub API. */ - const SERVER_URL = "https://api.github.com"; + const SERVER_URL = global.GITHUB_API_URL; /** * Search open issues from GitHub repositories. diff --git a/templates/vsc/common/declarative-agent-typespec/env/.env.dev.tpl b/templates/vsc/common/declarative-agent-typespec/env/.env.dev.tpl index 02e5b1e645c..12d0fcdb01c 100644 --- a/templates/vsc/common/declarative-agent-typespec/env/.env.dev.tpl +++ b/templates/vsc/common/declarative-agent-typespec/env/.env.dev.tpl @@ -3,6 +3,7 @@ # Built-in environment variables TEAMSFX_ENV=dev APP_NAME_SUFFIX=dev +GITHUB_API_URL=https://api.github.com {{#ShareEnabled}} AGENT_SCOPE=shared {{/ShareEnabled}} diff --git a/templates/vsc/common/declarative-agent-typespec/m365agents.yml.tpl b/templates/vsc/common/declarative-agent-typespec/m365agents.yml.tpl index 82dc2b17d06..04db362d6b2 100644 --- a/templates/vsc/common/declarative-agent-typespec/m365agents.yml.tpl +++ b/templates/vsc/common/declarative-agent-typespec/m365agents.yml.tpl @@ -25,11 +25,18 @@ provision: writeToEnvironmentFile: teamsAppId: TEAMS_APP_ID + # Install all dependencies (including TypeSpec for Microsoft 365 Copilot) - uses: cli/runNpmCommand name: install dependencies with: args: install --no-audit --progress=false + # Generates a TypeSpec version of the environment variables + - uses: cli/runNpmCommand + name: Generate TypeSpec environment variables + with: + args: run generate:variables -- ${{TEAMSFX_ENV}} + # Compile typespec files and generate necessary files for agent. # If you want to update the outputDir, please make sure the following paths are also updated. # 1. File paths in tspconfig.yaml. diff --git a/templates/vsc/common/declarative-agent-typespec/main.tsp.tpl b/templates/vsc/common/declarative-agent-typespec/main.tsp.tpl index cd87a925cd6..12da84181fc 100644 --- a/templates/vsc/common/declarative-agent-typespec/main.tsp.tpl +++ b/templates/vsc/common/declarative-agent-typespec/main.tsp.tpl @@ -2,10 +2,9 @@ import "@typespec/http"; import "@typespec/openapi3"; import "@microsoft/typespec-m365-copilot"; import "./actions.tsp"; +import "./variables.tsp"; -using TypeSpec.Http; using TypeSpec.M365.Copilot.Agents; -using TypeSpec.M365.Copilot.Actions; @agent( "{{appName}}", @@ -24,11 +23,5 @@ using TypeSpec.M365.Copilot.Actions; // }) namespace {{appName}} { - // Uncomment this part to add actions to the agent. - // @service - // @server(global.GitHubAPI.SERVER_URL) - // @actions(global.GitHubAPI.ACTIONS_METADATA) - // namespace GitHubAPIActions { - // op searchIssues is global.GitHubAPI.searchIssues; - // } + // op searchIssues is global.GitHubAPI.searchIssues; } \ No newline at end of file diff --git a/templates/vsc/common/declarative-agent-typespec/package.json.tpl b/templates/vsc/common/declarative-agent-typespec/package.json.tpl index dcfb996853d..3f0bc8d4be8 100644 --- a/templates/vsc/common/declarative-agent-typespec/package.json.tpl +++ b/templates/vsc/common/declarative-agent-typespec/package.json.tpl @@ -1,6 +1,9 @@ { "name": "{{SafeProjectNameLowerCase}}", "version": "1.0.0", + "scripts": { + "generate:variables": "node scripts/generate-variables.js" + }, "devDependencies": { "@microsoft/typespec-m365-copilot": "1.0.0-rc.4", "@typespec/compiler": "^1.0.0", diff --git a/templates/vsc/common/declarative-agent-typespec/scripts/generate-variable.js b/templates/vsc/common/declarative-agent-typespec/scripts/generate-variable.js new file mode 100644 index 00000000000..e9196e6d68d --- /dev/null +++ b/templates/vsc/common/declarative-agent-typespec/scripts/generate-variable.js @@ -0,0 +1,81 @@ +#!/usr/bin/env node + +// Self-executing script to generate variables.json and variables.tsp from env/.env.{environment} +(async function main() { + const fs = require("fs"); + const path = require("path"); + + try { + const envArg = process.argv[2] || "dev"; + + // src/agent is the parent of this scripts directory + const repoAgentDir = path.resolve(__dirname, ".."); + const envFilePath = path.join(repoAgentDir, "env", `.env.${envArg}`); + const outJsonPath = path.join(repoAgentDir, "variables.json"); + const outTspPath = path.join(repoAgentDir, "variables.tsp"); + + if (!fs.existsSync(envFilePath)) { + console.error(`Environment file not found: ${envFilePath}`); + console.error(`Usage: node generate-variables.js (e.g. dev, prod)`); + process.exitCode = 2; + return; + } + + const raw = fs.readFileSync(envFilePath, "utf8"); + + const vars = {}; + + for (const rawLine of raw.split(/\r?\n/)) { + const line = rawLine.trim(); + if (!line || line.startsWith("#")) continue; + + // support lines like KEY=value or export KEY=value + const cleaned = line.replace(/^export\s+/, ""); + const match = cleaned.match(/^([A-Za-z0-9_.-]+)\s*=\s*(.*)$/); + if (!match) continue; + + const key = match[1]; + let value = match[2] || ""; + + // strip surrounding single or double quotes + if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { + value = value.slice(1, -1); + } + + // Unescape common sequences (keep as-is if not needed) + value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r"); + + vars[key] = value; + } + + const tspLines = []; + tspLines.push("// Auto-generated by scripts/generate-variables.js - DO NOT EDIT"); + tspLines.push(`// Source: env/.env.${envArg}`); + tspLines.push(""); + + const keys = Object.keys(vars).sort(); + for (const originalKey of keys) { + const value = vars[originalKey]; + + // Convert original env key to a safe TypeSpec identifier: replace invalid chars with _ and ensure it doesn't start with a digit + let ident = originalKey.replace(/[^A-Za-z0-9_]/g, "_"); + if (/^[0-9]/.test(ident)) ident = "_" + ident; + + // Add a comment with the original env key for traceability + tspLines.push(`// ${originalKey}`); + + // Escape backslashes and double quotes for safe TS string literal + const escaped = value.replace(/\\/g, "\\\\").replace(/\"/g, "\\\""); + + tspLines.push(`const ${ident} = "${escaped}";`); + tspLines.push(""); + } + + fs.writeFileSync(outTspPath, tspLines.join("\n") + "\n", "utf8"); + + console.log(`Generated: ${outTspPath}`); + } catch (err) { + console.error("Failed to generate variables:", err && err.stack ? err.stack : err); + process.exitCode = 1; + } +})(); From 170aa26027dc9d0a8f653edc9f9ed48be6aac835 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Levert?= Date: Mon, 29 Sep 2025 15:20:19 -0400 Subject: [PATCH 03/24] feat: rename generate-variables files --- .../{generate-variable.js => generate-variables.js} | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) rename templates/vsc/common/declarative-agent-typespec/scripts/{generate-variable.js => generate-variables.js} (94%) diff --git a/templates/vsc/common/declarative-agent-typespec/scripts/generate-variable.js b/templates/vsc/common/declarative-agent-typespec/scripts/generate-variables.js similarity index 94% rename from templates/vsc/common/declarative-agent-typespec/scripts/generate-variable.js rename to templates/vsc/common/declarative-agent-typespec/scripts/generate-variables.js index e9196e6d68d..38fcc6601b2 100644 --- a/templates/vsc/common/declarative-agent-typespec/scripts/generate-variable.js +++ b/templates/vsc/common/declarative-agent-typespec/scripts/generate-variables.js @@ -38,7 +38,10 @@ let value = match[2] || ""; // strip surrounding single or double quotes - if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { value = value.slice(1, -1); } @@ -65,7 +68,7 @@ tspLines.push(`// ${originalKey}`); // Escape backslashes and double quotes for safe TS string literal - const escaped = value.replace(/\\/g, "\\\\").replace(/\"/g, "\\\""); + const escaped = value.replace(/\\/g, "\\\\").replace(/\"/g, '\\"'); tspLines.push(`const ${ident} = "${escaped}";`); tspLines.push(""); From 005841d5168d57fbc3ddd729cb5ca5d8262d4ffb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Levert?= Date: Thu, 9 Oct 2025 13:36:48 -0400 Subject: [PATCH 04/24] refactor: updated folder structure --- .../declarative-agent-typespec/.gitignore.tpl | 4 +- .../declarative-agent-typespec/AGENTS.md | 504 ++++++++++++++++++ .../declarative-agent-typespec/README.md | 19 +- .../{actions.tsp => actions/github.tsp} | 1 + .../m365agents.yml.tpl | 2 +- .../declarative-agent-typespec/main.tsp.tpl | 13 +- .../package.json.tpl | 3 +- .../prompts/instructions.tsp | 5 + ...{generate-variables.js => generate-env.js} | 16 +- 9 files changed, 543 insertions(+), 24 deletions(-) create mode 100644 templates/vsc/common/declarative-agent-typespec/AGENTS.md rename templates/vsc/common/declarative-agent-typespec/{actions.tsp => actions/github.tsp} (98%) create mode 100644 templates/vsc/common/declarative-agent-typespec/prompts/instructions.tsp rename templates/vsc/common/declarative-agent-typespec/scripts/{generate-variables.js => generate-env.js} (82%) diff --git a/templates/vsc/common/declarative-agent-typespec/.gitignore.tpl b/templates/vsc/common/declarative-agent-typespec/.gitignore.tpl index 5d7cd10226a..11e22b0f71f 100644 --- a/templates/vsc/common/declarative-agent-typespec/.gitignore.tpl +++ b/templates/vsc/common/declarative-agent-typespec/.gitignore.tpl @@ -14,5 +14,5 @@ node_modules/ # generated files appPackage/.generated -# generated variables -variables.tsp \ No newline at end of file +# generated environment variables +env.tsp \ No newline at end of file diff --git a/templates/vsc/common/declarative-agent-typespec/AGENTS.md b/templates/vsc/common/declarative-agent-typespec/AGENTS.md new file mode 100644 index 00000000000..31fa263d94c --- /dev/null +++ b/templates/vsc/common/declarative-agent-typespec/AGENTS.md @@ -0,0 +1,504 @@ +--- +description: 'Expert guidance for building declarative agents with TypeSpec for Microsoft 365 Copilot using Microsoft 365 Agents Toolkit' +applyTo: '**/*.tsp' +--- + +# TypeSpec Declarative Agent Development for Microsoft 365 Copilot + +## TypeSpec Instructions + +- Always use the latest stable version of TypeSpec compiler (`@typespec/compiler` v1.0.0+). +- Use the latest version of `@microsoft/typespec-m365-copilot` for agent-specific decorators and capabilities. +- Write clear and descriptive `@doc` comments for all operations, models, and namespaces. +- Follow semantic versioning for API operations when introducing breaking changes. + +## General Instructions + +- Make only high-confidence suggestions when reviewing TypeSpec definitions. +- Write TypeSpec with good maintainability practices, including comments explaining design decisions for complex schemas. +- Handle edge cases in action definitions and provide clear error documentation. +- For external APIs or services, document their purpose and integration points in comments. +- Always validate TypeSpec compilation (`npm run compile`) before provisioning. + +## Naming Conventions + +- Follow PascalCase for namespace names, model names, and operation names (e.g., `PoliciesAgent`, `AgentInfo`, `getAgents`). +- Use camelCase for parameter names and properties (e.g., `@path id: string`, `agentId: string`). +- Use descriptive names that reflect the operation's purpose (e.g., `getPolicies` instead of `getData`). +- Prefix enum members with their context when needed (e.g., `PolicyType.Compliance`, `AgentStatus.Active`). +- Use plural names for collection responses (e.g., `agents: AgentInfo[]` not `agent: AgentInfo[]`). + +## Formatting + +- Apply consistent indentation (2 or 4 spaces) throughout `.tsp` files. +- Group related imports together at the top of each file. +- Separate logical sections with comment dividers (e.g., `// --- Models ---`). +- Place decorators on separate lines above the element they modify. +- Use multiline format for operations with multiple parameters. +- Ensure consistent spacing around operators and colons in type annotations. +- Add a newline at the end of each file. + +## Project Setup and Structure + +- Guide users through creating a new declarative agent project using Microsoft 365 Agents Toolkit. +- Explain the purpose of each generated file: `main.tsp` (entry point), `prompts/*.tsp` (Prompts), `actions/*.tsp` (API operations), `env.tsp` (environment variables). +- Demonstrate how to organize code using separate namespaces for actions and models. +- Show proper separation of concerns: agent definition (main.tsp), actions (actions/*.tsp), data models (models.tsp). +- Explain the `m365agents.yml` configuration and how it orchestrates the build and deployment lifecycle. +- Guide users on environment-specific configurations using `env/.env.dev`, `env/.env.prod`. + +## TypeSpec Agent Fundamentals + +- Declare agents using the `@agent(name, description)` decorator in the main namespace. +- Link instructions to the agent using `@instructions(Prompts.INSTRUCTIONS)` decorator. +- Add conversation starters with `@conversationStarter(#{ title, text })` to guide users. Limit the number of conversation starters to 12. +- Expose actions as operations: `op getPolicies is PoliciesAPI.getPolicies`. +- Trust the TypeSpec type system and define strong contracts for all API operations. + +## Action Definitions + +- Define actions in dedicated namespaces decorated with `@service` and `@server(url)`. +- Use `@actions(metadata)` decorator to provide human-readable names and descriptions. +- Include `descriptionForModel` in action metadata to help the LLM understand when to invoke the action. +- Define operations using HTTP method decorators: `@get`, `@post`, `@put`, `@delete`. +- Specify routes with `@route("/path/{param}")` decorator. +- Use `@path`, `@query`, `@body`, `@header` decorators to define parameter sources. +- Provide default values for optional parameters (e.g., `@query type: PolicyType = PolicyType.Compliance`). +- Document all parameters with `@doc` comments explaining their purpose and constraints. +- Explain error scenarios in operation documentation (e.g., `404: Policy not found`). + +## Model Design + +- Create reusable models in a dedicated `models.tsp` file. +- Import the `models.tsp` file in the relevant `actions/*.tsp` files. +- Use descriptive model names that reflect their purpose (e.g., `PolicyInfo`, `PolicyResponse`). +- Define enums for fixed value sets (e.g., `enum AgentStatus { Active: "Active", Inactive: "Inactive" }`). +- Use proper TypeSpec primitive types: `string`, `int32`, `int64`, `float64`, `boolean`, `datetime`. +- Nest models when appropriate to avoid repetition and improve clarity. +- Add `@doc` comments to all model properties explaining their meaning and constraints. +- Use optional properties with `?` when fields may be absent (e.g., `days?: int32`). +- Leverage union types for fields that can have multiple types when needed. + +## Instruction Crafting + +- Write instructions in a dedicated `Prompts` namespace in `instructions.tsp`. +- Structure instructions with clear sections: GUIDELINES, EXAMPLES, SUGGESTIONS. +- Use strong directive keywords: **ALWAYS**, **NEVER**, **MUST** for critical rules. +- Provide concrete examples showing input, function calls, and expected output. +- Explain multi-step workflows explicitly (e.g., "First call getPolicies, then use the ID to call getStatus"). +- Include guidance on visualization when using CodeInterpreter capability. +- Document error handling strategies (e.g., "If action fails, explain the issue to the user"). +- Keep instructions concise and focused on behavior that the LLM should follow. +- Reference actions by their operation names to create clear associations. + +## Environment Configuration + +- Never manually edit `env.tsp` — it's auto-generated from `.env` files. +- Store environment-specific values in `env/.env.local`, `env/.env.dev`, `env/.env.prod`, etc. +- Run `npm run generate:env` after modifying `.env` files to regenerate `env.tsp`. +- All environment variables are in the `Environment` namespace. +- Reference environment constants using `Environment.CONSTANT_NAME` in TypeSpec files. +- Use uppercase snake_case for environment variable names (e.g., `APP_NAME_SHORT`, `POLICIES_API_ENDPOINT`). +- Never commit secrets or API keys in `.env` files — use secure storage (e.g., Azure Key Vault). +- Document required environment variables in the project README. + +## Validation and Testing + +- Always compile TypeSpec before provisioning: `npm run compile`. +- Fix all compilation errors and warnings before moving forward. +- Provision to local environment first: `atk provision --env local`. +- Test the agent in Microsoft 365 Copilot playground with diverse queries. +- Validate that the agent follows instructions (e.g., calls actions in the correct order). +- Test error scenarios: missing parameters, invalid IDs, API failures. +- Verify CodeInterpreter visualizations render correctly. +- Confirm conversation starters appear and work as expected. +- Test with multiple phrasings to ensure consistent agent behavior. +- Create test cases for critical workflows and validate after instruction changes. + +## Multi-Step Workflows + +- Guide the agent through multi-step workflows with explicit instructions. +- Example: "When the user asks about policies by agent name: 1- Call getAgents to get all agents IDs, 2- Match the user's requested name to an ID, 3- Call getAgentPolicy with that ID." +- Document prerequisite data fetching requirements in instructions. +- Explain fallback behavior when prerequisites fail (e.g., agent not found). +- Use action chaining judiciously to avoid overly complex workflows. + +## Capabilities Best Practices + +Microsoft 365 Copilot agents support multiple capabilities that extend the agent's functionality. Enable only the capabilities your agent needs. + +### General Capability Guidelines + +- Enable capabilities explicitly using `op capabilityName is AgentCapabilities.CapabilityName` syntax. +- Only enable capabilities that your agent will actively use — unnecessary capabilities increase complexity. +- Test each capability thoroughly in the Microsoft 365 Copilot playground before deployment. +- Document in your instructions how and when the agent should use each enabled capability. +- Combine capabilities strategically to create powerful multi-modal experiences. +- DO NOT add instructions related to the capabilities. + +### WebSearch Capability + +- **Enable (unscoped)**: `op webSearch is AgentCapabilities.WebSearch` +- **Enable (scoped)**: + + ```typescript + op webSearch is AgentCapabilities.WebSearch + ``` + +- **Purpose**: Allows the agent to search the public web for current information. +- **Scoping**: + - **CRITICAL**: Scoping is done via the `TSites` property in the capability definition, **NOT** in instructions. + - **NOT scoped**: Omit `TSites` array to allow searching all web content. + - **Scoped**: Specify `TSites` array with URLs to restrict web search to specific domains/sites. + - The agent will only search content from the specified URLs when scoped. + - A maximum of 4 URLs can be used. +- **Best practices**: + - Use when your agent needs real-time or recent information not available in your APIs. + - Scope to specific domains when you want to limit search to trusted sources (e.g., company documentation sites). + - Instruct the agent on when to prefer web search vs. internal actions (e.g., "Use web search only if internal data is unavailable"). + - Guide the agent to cite sources from web results in responses. + - Be aware that web search results may vary and are not under your control. + - Consider privacy implications when combining web data with internal data. +- **Example instruction**: "If the user asks about current events or recent news, use web search. Always cite your sources." +- **Example (scoped to Microsoft Learn)**: + + ```typescript + op webSearch is AgentCapabilities.WebSearch + ``` + +### OneDriveAndSharePoint Capability + +- **Enable (unscoped)**: `op oneDriveAndSharePoint is AgentCapabilities.OneDriveAndSharePoint` +- **Enable (scoped by URL)**: + + ```typescript + op od_sp is AgentCapabilities.OneDriveAndSharePoint + ``` + +- **Enable (scoped by SharePoint ID)**: + + ```typescript + op od_sp is AgentCapabilities.OneDriveAndSharePoint + ``` + +- **Purpose**: Enables the agent to search and access files in the user's OneDrive and SharePoint. +- **Scoping**: + - **CRITICAL**: Scoping is done via `TItemsByUrl` or `TItemsBySharePointIds` properties in the capability definition, **NOT** in instructions. + - **NOT scoped**: Omit both `TItemsByUrl` and `TItemsBySharePointIds` arrays to allow access to all OneDrive and SharePoint content available to the user. + - **Scoped by URL**: Use `TItemsByUrl` with full paths to SharePoint sites, document libraries, folders, or files. + - **Scoped by SharePoint ID**: Use `TItemsBySharePointIds` for more precise control using SharePoint internal IDs. + - URLs should be full paths (use "Copy direct link" in SharePoint: right-click → Details → Path → copy icon). +- **Best practices**: + - Use when your agent needs to work with user documents, spreadsheets, or presentations. + - Scope to specific sites/folders when you want to limit access to relevant content areas. + - Respect user permissions — the agent can only access files the user can access. + - Instruct the agent on file type preferences (e.g., "Prioritize Excel files for financial data"). + - Guide the agent to summarize file contents rather than returning raw data. + - Combine with CodeInterpreter to analyze data from files (e.g., Excel sheets). + - Be explicit about file name patterns or document types in instructions. +- **Example instruction**: "When the user asks about project documentation, search SharePoint for files matching the project name. Summarize key findings." +- **Example (scoped to Audits folder)**: + + ```typescript + op odsp is AgentCapabilities.OneDriveAndSharePoint + ``` + +### TeamsMessages Capability + +- **Enable (unscoped)**: `op teamsMessages is AgentCapabilities.TeamsMessages` +- **Enable (scoped)**: + + ```typescript + op teamsMessages is AgentCapabilities.TeamsMessages + ``` + +- **Purpose**: Allows the agent to use Teams channels, teams, and meeting chats as knowledge sources. +- **Scoping**: + - **CRITICAL**: Scoping is done via the `TUrls` property in the capability definition, **NOT** in instructions. + - **NOT scoped**: Omit `TUrls` array to allow access to all Teams channels, teams, meetings, 1:1 chats, and group chats available to the user. + - **Scoped**: Specify `TUrls` array with well-formed Teams URLs to restrict access to specific teams, channels, or chats. + - URLs must be valid Teams links (copy from Teams: team/channel → "..." menu → "Get link to channel/team"). +- **Best practices**: + - Use when your agent needs context from team conversations or project discussions. + - Scope to specific teams/channels when you want to limit to project-specific or department-specific conversations. + - Respect user permissions — the agent can only access messages the user can see. + - Instruct the agent to respect conversation context and thread relationships. + - Guide the agent to cite message authors and timestamps when referencing Teams content. + - Be aware of privacy and confidentiality when using Teams messages as knowledge. + - Consider combining with People capability to provide context about message authors. +- **Example instruction**: "When the user asks about project decisions, search Teams messages for discussions related to the topic. Cite the author and date of relevant messages." +- **Example (scoped to Engineering team)**: + + ```typescript + op teamsMessages is AgentCapabilities.TeamsMessages + ``` + +### Email Capability + +- **Enable (unscoped user mailbox)**: `op email is AgentCapabilities.Email` +- **Enable (scoped to specific folders)**: + + ```typescript + op email is AgentCapabilities.Email + ``` + +- **Enable (scoped to shared mailbox)**: + + ```typescript + op email is AgentCapabilities.Email + ``` + +- **Purpose**: Allows the agent to use email from the user's mailbox or a shared mailbox as a knowledge source. +- **Scoping**: + - **CRITICAL**: Scoping is done via `TFolders` and `TSharedMailbox` properties in the capability definition, **NOT** in instructions. + - **NOT scoped**: Omit `TFolders` array to allow access to the entire mailbox. + - **Scoped by folders**: Use `TFolders` array with folder IDs (e.g., "Inbox", "SentItems", "Drafts", custom folder names). + - **Scoped to shared mailbox**: Use `TSharedMailbox` property with the shared mailbox email address. + - Can combine shared mailbox with folder scoping for precise control. +- **Best practices**: + - Use when your agent needs to answer questions based on email correspondence. + - Scope to specific folders when you want to limit to relevant email categories (e.g., only customer support emails). + - Use shared mailboxes for team-wide email knowledge (e.g., support@, sales@). + - Respect user permissions — the agent can only access emails the user can access. + - Instruct the agent on how to handle sensitive information in emails. + - Guide the agent to cite sender, date, and subject when referencing emails. + - Be aware of privacy and confidentiality when using email as knowledge. + - Consider date range instructions to focus on recent communications. +- **Example instruction**: "When the user asks about customer inquiries, search emails for relevant correspondence. Summarize key points and include sender and date." +- **Example (scoped to support shared mailbox Inbox)**: + + ```typescript + op email is AgentCapabilities.Email + ``` + +### People Capability + +- **Enable**: `op people is AgentCapabilities.People` +- **Purpose**: Allows the agent to answer questions about individuals in the organization. +- **Scoping**: + - **NOTE**: This capability does not support scoping parameters. + - The agent can access information about all people in the organization directory that the user has permission to view. +- **Best practices**: + - Use when your agent needs to provide information about team members, org structure, or contact details. + - Instruct the agent on what people-related queries to handle (e.g., "Find contact info", "Who reports to X?"). + - Guide the agent to respect privacy and only share publicly available directory information. + - Combine with other capabilities to provide context (e.g., Teams messages + People to understand who said what). + - Be clear about what information is appropriate to share about people. + - Consider compliance and privacy regulations when enabling this capability. +- **Example instruction**: "When the user asks about team members or contacts, use the People capability to find relevant individuals. Provide names, roles, and contact information." +- **Example**: + + ```typescript + op people is AgentCapabilities.People; + ``` + +### GraphicArt Capability + +- **Enable**: `op graphicArt is AgentCapabilities.GraphicArt` +- **Purpose**: Enables the agent to generate images based on user prompts. +- **Scoping**: + - **NOTE**: This capability does not support scoping parameters. + - The agent can generate images based on any user prompt. +- **Best practices**: + - Use when your agent needs to create visual content, diagrams, or illustrations. + - Instruct the agent on when to generate images vs. describe content textually. + - Guide the agent on image style preferences (e.g., "Generate professional diagrams", "Use corporate color scheme"). + - Set expectations about generation time and potential limitations. + - Provide examples of appropriate image generation requests in instructions. + - Consider content policy and appropriateness of generated images. + - Inform users that generated images are AI-created and may need review. +- **Example instruction**: "When the user requests visual representations, diagrams, or illustrations, use the GraphicArt capability to generate images. Always describe what the image will show before generating." +- **Example**: + + ```typescript + op graphicArt is AgentCapabilities.GraphicArt; + ``` + +### GraphConnectors Capability + +- **Enable (unscoped)**: `op graphConnectors is AgentCapabilities.GraphConnectors` +- **Enable (scoped)**: + + ```typescript + op copilotConnectors is AgentCapabilities.GraphConnectors + ``` + +- **Purpose**: Allows the agent to search content ingested via Microsoft Graph connectors. +- **Scoping**: + - **CRITICAL**: Scoping is done via the `TConnections` property in the capability definition, **NOT** in instructions. + - **NOT scoped**: Omit `TConnections` array to allow access to all Graph connectors available to the user. + - **Scoped**: Specify `TConnections` array with connection IDs to restrict to specific connectors. + - Connection IDs can be found using Microsoft Graph API or admin tools. +- **Best practices**: + - Use when your organization has external data sources connected via Graph connectors. + - Scope to specific connectors when you want to limit to relevant external data sources. + - Ensure Graph connectors are properly configured and content is indexed before enabling. + - Document the types of data available through each Graph connector in your agent documentation. + - Guide the agent on when to prefer Graph connector data vs. direct API calls. + - Test with real indexed content to validate search quality and relevance. + - Monitor connector health and data freshness. +- **Example instruction**: "Search Graph connectors for customer support tickets when the user asks about customer issues. Provide ticket numbers and summaries." +- **Example (scoped to policies connector)**: + + ```typescript + op copilotConnectors is AgentCapabilities.GraphConnectors + ``` + +### CodeInterpreter Capability + +- **Enable**: `op codeInterpreter is AgentCapabilities.CodeInterpreter` +- **Purpose**: Enables the agent to write and execute Python code for data analysis and visualization. +- **Scoping**: + - **NOTE**: This capability does not support scoping parameters. + - The agent can execute Python code for any user request. +- **Best practices**: + - Use when your agent needs to perform calculations, data transformations, or create visualizations. + - Ensure data returned from actions is in a format compatible with Python libraries (JSON, CSV-style arrays). + - Instruct the agent on preferred visualization types (e.g., "Use line charts for trends, bar charts for comparisons"). + - Guide formatting requirements: "Always start y-axis at 0", "Add descriptive titles and axis labels". + - Test visualizations with sample data to confirm proper rendering. + - Provide fallback instructions if visualization fails (e.g., "If chart generation fails, provide data in a table"). + - Be aware of execution timeouts for long-running computations. + - Document any data preparation requirements (e.g., "Ensure dates are in ISO 8601 format"). +- **Example instruction**: "When displaying usage trends, use CodeInterpreter to create a line chart with dates on x-axis and usage count on y-axis. Always include a descriptive title." +- **Example**: + + ```typescript + op codeInterpreter is AgentCapabilities.CodeInterpreter; + ``` + +### Combining Capabilities + +- **Multi-modal experiences**: Combine capabilities to create powerful workflows. + - Example: OneDriveAndSharePoint + CodeInterpreter = "Find the sales Excel file and create a chart showing monthly revenue." + - Example: WebSearch + GraphConnectors = "Search internal knowledge base first, then supplement with web search if needed." + - Example: Email + People = "Find emails from my manager and provide their contact information." + - Example: TeamsMessages + People = "Search our team channel for decisions made by the engineering lead." + - Example: GraphicArt + WebSearch = "Search for the latest design trends and generate a mockup image." +- **Capability orchestration**: Provide clear instructions on the order of capability usage. + - Example: "First search SharePoint for the report. If not found, search Graph connectors. If still not found, use web search as a last resort." + - Example: "When asked about team communications, search Teams messages first, then check emails if no Teams conversations are found." +- **Performance considerations**: Multiple capabilities in one interaction may increase latency. +- **User experience**: Make capability usage transparent by mentioning sources (e.g., "Based on files in your SharePoint...", "According to Teams messages in the Engineering channel..."). + +## Error Handling + +- Document expected error codes in operation comments (e.g., `404: Not found`, `403: Forbidden`). +- Provide guidance in instructions for handling errors (e.g., "If 404, inform user the agent doesn't exist"). +- Recommend retry strategies for transient errors (5xx, timeouts) in instructions. +- Avoid retrying client errors (4xx) — they indicate bad requests. +- Return helpful error messages in API responses to aid agent responses. + +## API Versioning + +- Version API operations when introducing breaking changes (e.g., `getAgents_v1`, `getAgents_v2`). +- Use separate namespaces for different API versions if needed. +- Update agent definitions to reference the latest version explicitly. +- Maintain backward compatibility when possible to avoid disrupting existing agents. +- Document version differences and migration paths in comments. + +## Performance Optimization + +- Use efficient query patterns to minimize API latency. +- Implement pagination for operations that return large datasets. +- Cache frequently accessed data when appropriate (e.g., agent lists). +- Set reasonable defaults for query parameters (e.g., `limitDays: int32 = 720`). +- Monitor action latency and optimize slow endpoints. +- Explain performance considerations in operation documentation. + +## Deployment and DevOps + +- Provision to development environment first: `atk provision --env dev`. +- Use environment-specific configurations for each deployment stage. +- Implement health checks for backend APIs referenced by actions. +- Monitor agent usage and action invocations to identify issues. +- Set up CI/CD pipelines for automated compilation, validation, and deployment. +- Document rollback procedures in case of production issues. +- Explain deployment stages and environment promotion in project documentation. + +## Security Best Practices + +- Never hardcode secrets or API keys in TypeSpec files. +- Use environment variables for sensitive configuration. +- Implement proper authentication for backend APIs. +- Validate all user inputs in backend services, not just in TypeSpec definitions. +- Document authentication requirements in action metadata. +- Review generated manifests for unintended information disclosure. +- Follow principle of least privilege when granting API permissions. +- Conduct security reviews before deploying to production. + +## Documentation Standards + +- Add `@doc` comments to all public operations, models, and namespaces. +- Explain the purpose of each action in `descriptionForModel`. +- Document parameter constraints (e.g., "GUID format required", "positive integer"). +- Provide examples in comments for complex operations. +- Keep documentation up to date when changing TypeSpec definitions. +- Explain design decisions in comments when using non-obvious patterns. +- Reference related operations and models in documentation for discoverability. + +## Troubleshooting Common Issues + +- **Compilation fails**: Check for syntax errors, missing imports, type mismatches. Read error messages carefully for line numbers and hints. +- **Agent doesn't appear in Copilot**: Verify provisioning succeeded, check manifest validity, clear browser cache and re-authenticate. +- **Agent calls wrong action**: Strengthen instructions with explicit rules, improve `descriptionForModel`, add examples showing correct behavior. +- **CodeInterpreter doesn't visualize**: Ensure capability is enabled, add explicit visualization instructions, verify data format compatibility. +- **Provision hangs**: Check network connectivity, verify Microsoft 365 service health, simplify agent temporarily to isolate issue. +- **Action returns errors**: Review backend API logs, verify authentication, check parameter types and values. + +## Advanced Patterns + +- **Conditional operations**: Define multiple operations for different query types and guide selection via instructions. +- **Parameterized queries**: Offer flexibility with optional parameters and sensible defaults. +- **Multi-file projects**: Split large agents into logical modules (usage.tsp, feedback.tsp, admin.tsp). +- **Mock data for testing**: Create test harnesses that simulate Copilot requests to validate instruction adherence. +- **Telemetry integration**: Add telemetry to track action usage, latency, and errors for continuous improvement. + +## Best Practices Checklist + +- [ ] Compile TypeSpec without errors before provisioning +- [ ] Provide clear `@doc` comments for all operations and models +- [ ] Define strong types for all parameters and responses +- [ ] Include `descriptionForModel` for all actions +- [ ] Write explicit, testable instructions with examples +- [ ] Handle error scenarios gracefully +- [ ] Use environment variables for configuration +- [ ] Add conversation starters to guide users +- [ ] Version API operations for breaking changes +- [ ] Document deployment and rollback procedures +- [ ] Monitor agent usage and performance +- [ ] Gather user feedback and iterate on instructions +- [ ] Keep dependencies up to date + +--- + +**Remember**: TypeSpec is your contract. Write it clearly, compile it often, test it thoroughly, and deploy it confidently. The agent's quality reflects the clarity of your TypeSpec definitions and instructions. diff --git a/templates/vsc/common/declarative-agent-typespec/README.md b/templates/vsc/common/declarative-agent-typespec/README.md index e9da91841a0..cdb09959bed 100644 --- a/templates/vsc/common/declarative-agent-typespec/README.md +++ b/templates/vsc/common/declarative-agent-typespec/README.md @@ -29,7 +29,8 @@ With the declarative agent, you can build a custom version of Copilot that can b | Folder | Contents | | ------------ | ---------------------------------------------------------------------------------------- | | `.vscode` | VSCode files for debugging | -| `appPackage` | Templates for the application manifest, the GPT manifest and the API specification | +| `appPackage` | Templates for the application manifest, the manifest and the API specification | +| `scripts` | Scripts helping with automation across the build process | | `env` | Environment files | The following files can be customized and demonstrate an example implementation to get you started. @@ -50,13 +51,21 @@ The following are TypeSpec template files. You need to customize these files to | ------------- | ------------------------------------------------------------------------------------------- | | `main.tsp` | This is the root file of TSP files. Please manually update this file to add your own agent. | | `actions.tsp` | This is the actions file containing API endpoints to extend your declarative agent. | +| `env.tsp` | This is the file containing all environment variables to be used in TypeSpec files. | ## Extend the template -- [Add conversation starters](https://learn.microsoft.com/microsoft-365-copilot/extensibility/build-declarative-agents?tabs=ttk&tutorial-step=3): Conversation starters are hints that are displayed to the user to demonstrate how they can get started using the declarative agent. -- [Add web content](https://learn.microsoft.com/microsoft-365-copilot/extensibility/build-declarative-agents?tabs=ttk&tutorial-step=4) for the ability to search web information. -- [Add OneDrive and SharePoint content](https://learn.microsoft.com/microsoft-365-copilot/extensibility/build-declarative-agents?tabs=ttk&tutorial-step=5) as grounding knowledge for the agent. -- [Add Microsoft Copilot Connectors content](https://learn.microsoft.com/microsoft-365-copilot/extensibility/build-declarative-agents?tabs=ttk&tutorial-step=6) to ground agent with enterprise knowledge. +- [Add instructions](https://learn.microsoft.com/en-us/microsoft-365-copilot/extensibility/build-declarative-agents-typespec#add-instructions): Instructions change how an agent behaves. +- [Add conversation starters](https://learn.microsoft.com/en-us/microsoft-365-copilot/extensibility/build-declarative-agents-typespec#add-conversation-starters): Conversation starters are hints that are displayed to the user to demonstrate how they can get started using the declarative agent. +- [Add web content](https://learn.microsoft.com/en-us/microsoft-365-copilot/extensibility/build-declarative-agents-typespec#add-web-content): The web search capability enables agents to use the search index in Bing to respond to user prompts. +- [Add OneDrive and SharePoint content](https://learn.microsoft.com/en-us/microsoft-365-copilot/extensibility/build-declarative-agents-typespec#add-onedrive-and-sharepoint-content) as grounding knowledge for the agent. +- [Add Teams messages](https://learn.microsoft.com/en-us/microsoft-365-copilot/extensibility/build-declarative-agents-typespec#add-teams-messages): The Teams messages capability allows the agent to use Teams channels, team, and meeting chat as knowledge. +- [Add people knowledge](https://learn.microsoft.com/en-us/microsoft-365-copilot/extensibility/build-declarative-agents-typespec#add-people-knowledge): The people capability allows you to scope your agent to answer questions about individuals in an organization. +- [Add email knowledge](https://learn.microsoft.com/en-us/microsoft-365-copilot/extensibility/build-declarative-agents-typespec#add-email-knowledge): The email capability allows you to scope your agent to use email from the user's mailbox or a shared mailbox as a knowledge source. +- [Add image generator](https://learn.microsoft.com/en-us/microsoft-365-copilot/extensibility/build-declarative-agents-typespec#add-image-generator): The image generator capability enables agents to generate images based on user prompts. +- [Add code interpreter](https://learn.microsoft.com/en-us/microsoft-365-copilot/extensibility/build-declarative-agents-typespec#add-code-interpreter): The code interpreter capability is an advanced tool designed to solve complex tasks via Python code. +- [Add Copilot Connectors content](https://learn.microsoft.com/en-us/microsoft-365-copilot/extensibility/build-declarative-agents-typespec#add-copilot-connectors-content): You can add items ingested by a Copilot connector to the available knowledge for the agent. +- [Add actions](https://learn.microsoft.com/en-us/microsoft-365-copilot/extensibility/build-api-plugins-typespec): API plugins are custom actions for declarative agents that connect a REST API with an OpenAPI specification to Microsoft 365 Copilot. ## Addition information and references diff --git a/templates/vsc/common/declarative-agent-typespec/actions.tsp b/templates/vsc/common/declarative-agent-typespec/actions/github.tsp similarity index 98% rename from templates/vsc/common/declarative-agent-typespec/actions.tsp rename to templates/vsc/common/declarative-agent-typespec/actions/github.tsp index 1945766b58f..209f8b8b8fe 100644 --- a/templates/vsc/common/declarative-agent-typespec/actions.tsp +++ b/templates/vsc/common/declarative-agent-typespec/actions/github.tsp @@ -1,5 +1,6 @@ import "@typespec/http"; import "@microsoft/typespec-m365-copilot"; +import "../env.tsp"; using TypeSpec.Http; using TypeSpec.M365.Copilot.Actions; diff --git a/templates/vsc/common/declarative-agent-typespec/m365agents.yml.tpl b/templates/vsc/common/declarative-agent-typespec/m365agents.yml.tpl index 04db362d6b2..96f0589ba44 100644 --- a/templates/vsc/common/declarative-agent-typespec/m365agents.yml.tpl +++ b/templates/vsc/common/declarative-agent-typespec/m365agents.yml.tpl @@ -35,7 +35,7 @@ provision: - uses: cli/runNpmCommand name: Generate TypeSpec environment variables with: - args: run generate:variables -- ${{TEAMSFX_ENV}} + args: run generate:env -- ${{TEAMSFX_ENV}} # Compile typespec files and generate necessary files for agent. # If you want to update the outputDir, please make sure the following paths are also updated. diff --git a/templates/vsc/common/declarative-agent-typespec/main.tsp.tpl b/templates/vsc/common/declarative-agent-typespec/main.tsp.tpl index 12da84181fc..a3f2a978b2e 100644 --- a/templates/vsc/common/declarative-agent-typespec/main.tsp.tpl +++ b/templates/vsc/common/declarative-agent-typespec/main.tsp.tpl @@ -1,8 +1,9 @@ import "@typespec/http"; import "@typespec/openapi3"; import "@microsoft/typespec-m365-copilot"; -import "./actions.tsp"; -import "./variables.tsp"; +import "./actions/github.tsp"; +import "./prompts/instructions.tsp"; +import "./env.tsp"; using TypeSpec.M365.Copilot.Agents; @@ -10,18 +11,14 @@ using TypeSpec.M365.Copilot.Agents; "{{appName}}", "Declarative agent created with Microsoft 365 Agents Toolkit and TypeSpec for Microsoft 365 Copilot." ) - -@instructions(""" - You are a declarative agent and were created with Microsoft 365 Agents Toolkit and TypeSpec for Microsoft 365 Copilot. -""") - +@instructions(Prompts.INSTRUCTIONS) // Uncomment this part to add a conversation starter to the agent. // This will be shown to the user when the agent is first created. // @conversationStarter(#{ // title: "Get latest issues", // text: "Get the latest issues from GitHub" // }) - namespace {{appName}} { + // Uncomment this part to include custom actions in the agent // op searchIssues is global.GitHubAPI.searchIssues; } \ No newline at end of file diff --git a/templates/vsc/common/declarative-agent-typespec/package.json.tpl b/templates/vsc/common/declarative-agent-typespec/package.json.tpl index 3f0bc8d4be8..42dd54873b6 100644 --- a/templates/vsc/common/declarative-agent-typespec/package.json.tpl +++ b/templates/vsc/common/declarative-agent-typespec/package.json.tpl @@ -2,7 +2,8 @@ "name": "{{SafeProjectNameLowerCase}}", "version": "1.0.0", "scripts": { - "generate:variables": "node scripts/generate-variables.js" + "compile": "tsp compile ./main.tsp --config ./tspconfig.yaml", + "generate:env": "node scripts/generate-env.js" }, "devDependencies": { "@microsoft/typespec-m365-copilot": "1.0.0-rc.4", diff --git a/templates/vsc/common/declarative-agent-typespec/prompts/instructions.tsp b/templates/vsc/common/declarative-agent-typespec/prompts/instructions.tsp new file mode 100644 index 00000000000..7c98c6bbac4 --- /dev/null +++ b/templates/vsc/common/declarative-agent-typespec/prompts/instructions.tsp @@ -0,0 +1,5 @@ +namespace Prompts { + const INSTRUCTIONS = """ + You are a declarative agent and were created with Microsoft 365 Agents Toolkit and TypeSpec for Microsoft 365 Copilot. + """; +} \ No newline at end of file diff --git a/templates/vsc/common/declarative-agent-typespec/scripts/generate-variables.js b/templates/vsc/common/declarative-agent-typespec/scripts/generate-env.js similarity index 82% rename from templates/vsc/common/declarative-agent-typespec/scripts/generate-variables.js rename to templates/vsc/common/declarative-agent-typespec/scripts/generate-env.js index 38fcc6601b2..53ff4cb1f89 100644 --- a/templates/vsc/common/declarative-agent-typespec/scripts/generate-variables.js +++ b/templates/vsc/common/declarative-agent-typespec/scripts/generate-env.js @@ -11,12 +11,11 @@ // src/agent is the parent of this scripts directory const repoAgentDir = path.resolve(__dirname, ".."); const envFilePath = path.join(repoAgentDir, "env", `.env.${envArg}`); - const outJsonPath = path.join(repoAgentDir, "variables.json"); - const outTspPath = path.join(repoAgentDir, "variables.tsp"); + const outTspPath = path.join(repoAgentDir, "env.tsp"); if (!fs.existsSync(envFilePath)) { console.error(`Environment file not found: ${envFilePath}`); - console.error(`Usage: node generate-variables.js (e.g. dev, prod)`); + console.error(`Usage: node generate-env.js (e.g. dev, prod)`); process.exitCode = 2; return; } @@ -52,9 +51,10 @@ } const tspLines = []; - tspLines.push("// Auto-generated by scripts/generate-variables.js - DO NOT EDIT"); + tspLines.push("// Auto-generated by scripts/generate-env.js - DO NOT EDIT"); tspLines.push(`// Source: env/.env.${envArg}`); tspLines.push(""); + tspLines.push("namespace Environment {"); const keys = Object.keys(vars).sort(); for (const originalKey of keys) { @@ -65,20 +65,22 @@ if (/^[0-9]/.test(ident)) ident = "_" + ident; // Add a comment with the original env key for traceability - tspLines.push(`// ${originalKey}`); + tspLines.push(` // ${originalKey}`); // Escape backslashes and double quotes for safe TS string literal const escaped = value.replace(/\\/g, "\\\\").replace(/\"/g, '\\"'); - tspLines.push(`const ${ident} = "${escaped}";`); + tspLines.push(` const ${ident} = "${escaped}";`); tspLines.push(""); } + tspLines.push("}"); + fs.writeFileSync(outTspPath, tspLines.join("\n") + "\n", "utf8"); console.log(`Generated: ${outTspPath}`); } catch (err) { - console.error("Failed to generate variables:", err && err.stack ? err.stack : err); + console.error("Failed to generate environment variables:", err && err.stack ? err.stack : err); process.exitCode = 1; } })(); From 963f786ce64737d09354bdeb6ccca9cc9879065f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Levert?= Date: Thu, 9 Oct 2025 13:39:15 -0400 Subject: [PATCH 05/24] fix: add 'Install dependencies' task to tasks.json.tpl and update README for clarity --- .../.vscode/tasks.json.tpl | 10 ++++++++++ .../common/declarative-agent-typespec/README.md | 15 +++++++++------ .../declarative-agent-typespec/actions/github.tsp | 2 +- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/templates/vsc/common/declarative-agent-typespec/.vscode/tasks.json.tpl b/templates/vsc/common/declarative-agent-typespec/.vscode/tasks.json.tpl index 124c27592a9..a5b16add157 100644 --- a/templates/vsc/common/declarative-agent-typespec/.vscode/tasks.json.tpl +++ b/templates/vsc/common/declarative-agent-typespec/.vscode/tasks.json.tpl @@ -32,5 +32,15 @@ } } {{/ShareEnabled}} + { + "label": "Install dependencies", + "type": "shell", + "command": "npm install", + "presentation": { + "reveal": "silent", + "panel": "new" + }, + "runOptions": { "runOn": "folderOpen" } + } ] } \ No newline at end of file diff --git a/templates/vsc/common/declarative-agent-typespec/README.md b/templates/vsc/common/declarative-agent-typespec/README.md index cdb09959bed..9c007e358f7 100644 --- a/templates/vsc/common/declarative-agent-typespec/README.md +++ b/templates/vsc/common/declarative-agent-typespec/README.md @@ -30,8 +30,10 @@ With the declarative agent, you can build a custom version of Copilot that can b | ------------ | ---------------------------------------------------------------------------------------- | | `.vscode` | VSCode files for debugging | | `appPackage` | Templates for the application manifest, the manifest and the API specification | -| `scripts` | Scripts helping with automation across the build process | +| `actions` | All action files representing API surfaces | | `env` | Environment files | +| `prompts` | All prompt files used for instructions | +| `scripts` | Scripts helping with automation across the build process | The following files can be customized and demonstrate an example implementation to get you started. @@ -47,11 +49,12 @@ The following are Microsoft 365 Agents Toolkit specific project files. You can [ The following are TypeSpec template files. You need to customize these files to configure your agent. -| File | Contents | -| ------------- | ------------------------------------------------------------------------------------------- | -| `main.tsp` | This is the root file of TSP files. Please manually update this file to add your own agent. | -| `actions.tsp` | This is the actions file containing API endpoints to extend your declarative agent. | -| `env.tsp` | This is the file containing all environment variables to be used in TypeSpec files. | +| File | Contents | +| --------------- | ------------------------------------------------------------------------------------------- | +| `main.tsp` | This is the root file of TSP files. Please manually update this file to add your own agent. | +| `actions/*.tsp` | These are action files containing API endpoints to extend your declarative agent. | +| `prompts/*.tsp` | These are prompt files used for instructions inf your declarative agent. | +| `env.tsp` | This is the file containing all environment variables to be used in TypeSpec files. | ## Extend the template diff --git a/templates/vsc/common/declarative-agent-typespec/actions/github.tsp b/templates/vsc/common/declarative-agent-typespec/actions/github.tsp index 209f8b8b8fe..4659aa8f919 100644 --- a/templates/vsc/common/declarative-agent-typespec/actions/github.tsp +++ b/templates/vsc/common/declarative-agent-typespec/actions/github.tsp @@ -23,7 +23,7 @@ namespace GitHubAPI { /** * The base URL for the GitHub API. */ - const SERVER_URL = global.GITHUB_API_URL; + const SERVER_URL = Environment.GITHUB_API_URL; /** * Search open issues from GitHub repositories. From 8f05bcd900eab8f6107e2c05b0f7d8a470be7eb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Levert?= Date: Thu, 9 Oct 2025 14:08:55 -0400 Subject: [PATCH 06/24] fix: adding build folder to the ignore list --- templates/vsc/common/declarative-agent-typespec/.gitignore.tpl | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/vsc/common/declarative-agent-typespec/.gitignore.tpl b/templates/vsc/common/declarative-agent-typespec/.gitignore.tpl index 11e22b0f71f..8289ccb8e07 100644 --- a/templates/vsc/common/declarative-agent-typespec/.gitignore.tpl +++ b/templates/vsc/common/declarative-agent-typespec/.gitignore.tpl @@ -3,6 +3,7 @@ env/.env.*.user env/.env.local .localConfigs appPackage/build +build # dependencies node_modules/ From 911aacbde2730b434da7a9a53699f8d68c53e4a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Levert?= Date: Fri, 10 Oct 2025 13:04:43 -0400 Subject: [PATCH 07/24] feat: restructure TypeSpec agent template and update paths for better organization --- .../declarative-agent-typespec/.gitignore.tpl | 2 +- .../.vscode/settings.json | 3 +- .../declarative-agent-typespec/README.md | 28 +++++++++---------- .../m365agents.yml.tpl | 2 +- .../package.json.tpl | 2 +- .../scripts/generate-env.js | 2 +- .../{ => src/agent}/actions/github.tsp | 0 .../{ => src/agent}/main.tsp.tpl | 0 .../{ => src/agent}/prompts/instructions.tsp | 0 9 files changed, 20 insertions(+), 19 deletions(-) rename templates/vsc/common/declarative-agent-typespec/{ => src/agent}/actions/github.tsp (100%) rename templates/vsc/common/declarative-agent-typespec/{ => src/agent}/main.tsp.tpl (100%) rename templates/vsc/common/declarative-agent-typespec/{ => src/agent}/prompts/instructions.tsp (100%) diff --git a/templates/vsc/common/declarative-agent-typespec/.gitignore.tpl b/templates/vsc/common/declarative-agent-typespec/.gitignore.tpl index 8289ccb8e07..5bc64fdc3e6 100644 --- a/templates/vsc/common/declarative-agent-typespec/.gitignore.tpl +++ b/templates/vsc/common/declarative-agent-typespec/.gitignore.tpl @@ -16,4 +16,4 @@ node_modules/ # generated files appPackage/.generated # generated environment variables -env.tsp \ No newline at end of file +src/agent/env.tsp \ No newline at end of file diff --git a/templates/vsc/common/declarative-agent-typespec/.vscode/settings.json b/templates/vsc/common/declarative-agent-typespec/.vscode/settings.json index de35f148395..a4e385dd1e8 100644 --- a/templates/vsc/common/declarative-agent-typespec/.vscode/settings.json +++ b/templates/vsc/common/declarative-agent-typespec/.vscode/settings.json @@ -9,6 +9,7 @@ } ], "files.readonlyInclude": { - "appPackage/.generated/**/*": true + "appPackage/.generated/**/*": true, + "src/agent/env.tsp": true } } diff --git a/templates/vsc/common/declarative-agent-typespec/README.md b/templates/vsc/common/declarative-agent-typespec/README.md index 9c007e358f7..a249ea351a3 100644 --- a/templates/vsc/common/declarative-agent-typespec/README.md +++ b/templates/vsc/common/declarative-agent-typespec/README.md @@ -26,14 +26,14 @@ With the declarative agent, you can build a custom version of Copilot that can b ## What's included in the template -| Folder | Contents | -| ------------ | ---------------------------------------------------------------------------------------- | -| `.vscode` | VSCode files for debugging | -| `appPackage` | Templates for the application manifest, the manifest and the API specification | -| `actions` | All action files representing API surfaces | -| `env` | Environment files | -| `prompts` | All prompt files used for instructions | -| `scripts` | Scripts helping with automation across the build process | +| Folder | Contents | +| -------------------- | ---------------------------------------------------------------------------------------- | +| `.vscode` | VSCode files for debugging | +| `appPackage` | Templates for the application manifest, the manifest and the API specification | +| `src/agent/actions` | All action files representing API surfaces | +| `env` | Environment files | +| `src/agent/prompts` | All prompt files used for instructions | +| `scripts` | Scripts helping with automation across the build process | The following files can be customized and demonstrate an example implementation to get you started. @@ -49,12 +49,12 @@ The following are Microsoft 365 Agents Toolkit specific project files. You can [ The following are TypeSpec template files. You need to customize these files to configure your agent. -| File | Contents | -| --------------- | ------------------------------------------------------------------------------------------- | -| `main.tsp` | This is the root file of TSP files. Please manually update this file to add your own agent. | -| `actions/*.tsp` | These are action files containing API endpoints to extend your declarative agent. | -| `prompts/*.tsp` | These are prompt files used for instructions inf your declarative agent. | -| `env.tsp` | This is the file containing all environment variables to be used in TypeSpec files. | +| File | Contents | +| ------------------------- | ------------------------------------------------------------------------------------------- | +| `src/agent/main.tsp` | This is the root file of TSP files. Please manually update this file to add your own agent. | +| `src/agent/actions/*.tsp` | These are action files containing API endpoints to extend your declarative agent. | +| `src/agent/prompts/*.tsp` | These are prompt files used for instructions inf your declarative agent. | +| `src/agent/env.tsp` | This is the file containing all environment variables to be used in TypeSpec files. | ## Extend the template diff --git a/templates/vsc/common/declarative-agent-typespec/m365agents.yml.tpl b/templates/vsc/common/declarative-agent-typespec/m365agents.yml.tpl index 96f0589ba44..653a2887aa2 100644 --- a/templates/vsc/common/declarative-agent-typespec/m365agents.yml.tpl +++ b/templates/vsc/common/declarative-agent-typespec/m365agents.yml.tpl @@ -44,7 +44,7 @@ provision: # 3. manifestPath in teamsApp/zipAppPackage action. Please set the value to the same as manifestPath in this action. - uses: typeSpec/compile with: - path: ./main.tsp + path: ./src/agent/main.tsp manifestPath: ./appPackage/manifest.json outputDir: ./appPackage/.generated typeSpecConfigPath: ./tspconfig.yaml diff --git a/templates/vsc/common/declarative-agent-typespec/package.json.tpl b/templates/vsc/common/declarative-agent-typespec/package.json.tpl index 42dd54873b6..324da23f39e 100644 --- a/templates/vsc/common/declarative-agent-typespec/package.json.tpl +++ b/templates/vsc/common/declarative-agent-typespec/package.json.tpl @@ -2,7 +2,7 @@ "name": "{{SafeProjectNameLowerCase}}", "version": "1.0.0", "scripts": { - "compile": "tsp compile ./main.tsp --config ./tspconfig.yaml", + "compile": "tsp compile ./src/agent/main.tsp --config ./tspconfig.yaml", "generate:env": "node scripts/generate-env.js" }, "devDependencies": { diff --git a/templates/vsc/common/declarative-agent-typespec/scripts/generate-env.js b/templates/vsc/common/declarative-agent-typespec/scripts/generate-env.js index 53ff4cb1f89..ac55cf4f674 100644 --- a/templates/vsc/common/declarative-agent-typespec/scripts/generate-env.js +++ b/templates/vsc/common/declarative-agent-typespec/scripts/generate-env.js @@ -11,7 +11,7 @@ // src/agent is the parent of this scripts directory const repoAgentDir = path.resolve(__dirname, ".."); const envFilePath = path.join(repoAgentDir, "env", `.env.${envArg}`); - const outTspPath = path.join(repoAgentDir, "env.tsp"); + const outTspPath = path.join(repoAgentDir, "src/agent/env.tsp"); if (!fs.existsSync(envFilePath)) { console.error(`Environment file not found: ${envFilePath}`); diff --git a/templates/vsc/common/declarative-agent-typespec/actions/github.tsp b/templates/vsc/common/declarative-agent-typespec/src/agent/actions/github.tsp similarity index 100% rename from templates/vsc/common/declarative-agent-typespec/actions/github.tsp rename to templates/vsc/common/declarative-agent-typespec/src/agent/actions/github.tsp diff --git a/templates/vsc/common/declarative-agent-typespec/main.tsp.tpl b/templates/vsc/common/declarative-agent-typespec/src/agent/main.tsp.tpl similarity index 100% rename from templates/vsc/common/declarative-agent-typespec/main.tsp.tpl rename to templates/vsc/common/declarative-agent-typespec/src/agent/main.tsp.tpl diff --git a/templates/vsc/common/declarative-agent-typespec/prompts/instructions.tsp b/templates/vsc/common/declarative-agent-typespec/src/agent/prompts/instructions.tsp similarity index 100% rename from templates/vsc/common/declarative-agent-typespec/prompts/instructions.tsp rename to templates/vsc/common/declarative-agent-typespec/src/agent/prompts/instructions.tsp From 2535e2b54224e10bdefcf97ff46eac84859424bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Levert?= Date: Thu, 23 Oct 2025 15:10:24 -0400 Subject: [PATCH 08/24] fix: update @microsoft/typespec-m365-copilot version to 1.0.0-rc.5 --- .../vsc/common/declarative-agent-typespec/package.json.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/vsc/common/declarative-agent-typespec/package.json.tpl b/templates/vsc/common/declarative-agent-typespec/package.json.tpl index 324da23f39e..8b4af243969 100644 --- a/templates/vsc/common/declarative-agent-typespec/package.json.tpl +++ b/templates/vsc/common/declarative-agent-typespec/package.json.tpl @@ -6,7 +6,7 @@ "generate:env": "node scripts/generate-env.js" }, "devDependencies": { - "@microsoft/typespec-m365-copilot": "1.0.0-rc.4", + "@microsoft/typespec-m365-copilot": "1.0.0-rc.5", "@typespec/compiler": "^1.0.0", "@typespec/http": "^1.0.0", "@typespec/openapi": "^1.0.0", From 0be69f8cc2f09a8662866b63bc242b96f54a9ffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Levert?= Date: Thu, 23 Oct 2025 15:17:00 -0400 Subject: [PATCH 09/24] fix: update TypeSpec capability property names for consistency and clarity --- .../declarative-agent-typespec/AGENTS.md | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/templates/vsc/common/declarative-agent-typespec/AGENTS.md b/templates/vsc/common/declarative-agent-typespec/AGENTS.md index 31fa263d94c..eb79dc437bc 100644 --- a/templates/vsc/common/declarative-agent-typespec/AGENTS.md +++ b/templates/vsc/common/declarative-agent-typespec/AGENTS.md @@ -177,7 +177,7 @@ Microsoft 365 Copilot agents support multiple capabilities that extend the agent - **Enable (scoped by URL)**: ```typescript - op od_sp is AgentCapabilities.OneDriveAndSharePoint @@ -186,17 +186,17 @@ Microsoft 365 Copilot agents support multiple capabilities that extend the agent - **Enable (scoped by SharePoint ID)**: ```typescript - op od_sp is AgentCapabilities.OneDriveAndSharePoint ``` - **Purpose**: Enables the agent to search and access files in the user's OneDrive and SharePoint. - **Scoping**: - - **CRITICAL**: Scoping is done via `TItemsByUrl` or `TItemsBySharePointIds` properties in the capability definition, **NOT** in instructions. - - **NOT scoped**: Omit both `TItemsByUrl` and `TItemsBySharePointIds` arrays to allow access to all OneDrive and SharePoint content available to the user. - - **Scoped by URL**: Use `TItemsByUrl` with full paths to SharePoint sites, document libraries, folders, or files. - - **Scoped by SharePoint ID**: Use `TItemsBySharePointIds` for more precise control using SharePoint internal IDs. + - **CRITICAL**: Scoping is done via `ItemsByUrl` or `ItemsBySharePointIds` properties in the capability definition, **NOT** in instructions. + - **NOT scoped**: Omit both `ItemsByUrl` and `ItemsBySharePointIds` arrays to allow access to all OneDrive and SharePoint content available to the user. + - **Scoped by URL**: Use `ItemsByUrl` with full paths to SharePoint sites, document libraries, folders, or files. + - **Scoped by SharePoint ID**: Use `ItemsBySharePointIds` for more precise control using SharePoint internal IDs. - URLs should be full paths (use "Copy direct link" in SharePoint: right-click → Details → Path → copy icon). - **Best practices**: - Use when your agent needs to work with user documents, spreadsheets, or presentations. @@ -210,7 +210,7 @@ Microsoft 365 Copilot agents support multiple capabilities that extend the agent - **Example (scoped to Audits folder)**: ```typescript - op odsp is AgentCapabilities.OneDriveAndSharePoint ``` @@ -221,7 +221,7 @@ Microsoft 365 Copilot agents support multiple capabilities that extend the agent - **Enable (scoped)**: ```typescript - op teamsMessages is AgentCapabilities.TeamsMessages @@ -229,9 +229,9 @@ Microsoft 365 Copilot agents support multiple capabilities that extend the agent - **Purpose**: Allows the agent to use Teams channels, teams, and meeting chats as knowledge sources. - **Scoping**: - - **CRITICAL**: Scoping is done via the `TUrls` property in the capability definition, **NOT** in instructions. - - **NOT scoped**: Omit `TUrls` array to allow access to all Teams channels, teams, meetings, 1:1 chats, and group chats available to the user. - - **Scoped**: Specify `TUrls` array with well-formed Teams URLs to restrict access to specific teams, channels, or chats. + - **CRITICAL**: Scoping is done via the `TeamsMessagesByUrl` property in the capability definition, **NOT** in instructions. + - **NOT scoped**: Omit `TeamsMessagesByUrl` array to allow access to all Teams channels, teams, meetings, 1:1 chats, and group chats available to the user. + - **Scoped**: Specify `TeamsMessagesByUrl` array with well-formed Teams URLs to restrict access to specific teams, channels, or chats. - URLs must be valid Teams links (copy from Teams: team/channel → "..." menu → "Get link to channel/team"). - **Best practices**: - Use when your agent needs context from team conversations or project discussions. @@ -245,7 +245,7 @@ Microsoft 365 Copilot agents support multiple capabilities that extend the agent - **Example (scoped to Engineering team)**: ```typescript - op teamsMessages is AgentCapabilities.TeamsMessages ``` @@ -256,7 +256,7 @@ Microsoft 365 Copilot agents support multiple capabilities that extend the agent - **Enable (scoped to specific folders)**: ```typescript - op email is AgentCapabilities.Email @@ -265,17 +265,17 @@ Microsoft 365 Copilot agents support multiple capabilities that extend the agent - **Enable (scoped to shared mailbox)**: ```typescript - op email is AgentCapabilities.Email ``` - **Purpose**: Allows the agent to use email from the user's mailbox or a shared mailbox as a knowledge source. - **Scoping**: - - **CRITICAL**: Scoping is done via `TFolders` and `TSharedMailbox` properties in the capability definition, **NOT** in instructions. - - **NOT scoped**: Omit `TFolders` array to allow access to the entire mailbox. - - **Scoped by folders**: Use `TFolders` array with folder IDs (e.g., "Inbox", "SentItems", "Drafts", custom folder names). - - **Scoped to shared mailbox**: Use `TSharedMailbox` property with the shared mailbox email address. + - **CRITICAL**: Scoping is done via `Folders` and `SharedMailbox` properties in the capability definition, **NOT** in instructions. + - **NOT scoped**: Omit `Folders` array to allow access to the entire mailbox. + - **Scoped by folders**: Use `Folders` array with folder IDs (e.g., "Inbox", "SentItems", "Drafts", custom folder names). + - **Scoped to shared mailbox**: Use `SharedMailbox` property with the shared mailbox email address. - Can combine shared mailbox with folder scoping for precise control. - **Best practices**: - Use when your agent needs to answer questions based on email correspondence. @@ -290,7 +290,7 @@ Microsoft 365 Copilot agents support multiple capabilities that extend the agent - **Example (scoped to support shared mailbox Inbox)**: ```typescript - op email is AgentCapabilities.Email ``` @@ -338,13 +338,13 @@ Microsoft 365 Copilot agents support multiple capabilities that extend the agent op graphicArt is AgentCapabilities.GraphicArt; ``` -### GraphConnectors Capability +### CopilotConnectors Capability -- **Enable (unscoped)**: `op graphConnectors is AgentCapabilities.GraphConnectors` +- **Enable (unscoped)**: `op graphConnectors is AgentCapabilities.CopilotConnectors` - **Enable (scoped)**: ```typescript - op copilotConnectors is AgentCapabilities.GraphConnectors @@ -352,9 +352,9 @@ Microsoft 365 Copilot agents support multiple capabilities that extend the agent - **Purpose**: Allows the agent to search content ingested via Microsoft Graph connectors. - **Scoping**: - - **CRITICAL**: Scoping is done via the `TConnections` property in the capability definition, **NOT** in instructions. - - **NOT scoped**: Omit `TConnections` array to allow access to all Graph connectors available to the user. - - **Scoped**: Specify `TConnections` array with connection IDs to restrict to specific connectors. + - **CRITICAL**: Scoping is done via the `Connections` property in the capability definition, **NOT** in instructions. + - **NOT scoped**: Omit `Connections` array to allow access to all Graph connectors available to the user. + - **Scoped**: Specify `Connections` array with connection IDs to restrict to specific connectors. - Connection IDs can be found using Microsoft Graph API or admin tools. - **Best practices**: - Use when your organization has external data sources connected via Graph connectors. @@ -368,7 +368,7 @@ Microsoft 365 Copilot agents support multiple capabilities that extend the agent - **Example (scoped to policies connector)**: ```typescript - op copilotConnectors is AgentCapabilities.GraphConnectors ``` From 980cc2a9eda2f993ef3cc34b3c69de7fec21e2c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Levert?= Date: Thu, 23 Oct 2025 21:50:27 -0400 Subject: [PATCH 10/24] feat: add detailed documentation for agent decorators and capabilities in AGENTS.md --- .../declarative-agent-typespec/AGENTS.md | 257 +++++++++++++++++- 1 file changed, 249 insertions(+), 8 deletions(-) diff --git a/templates/vsc/common/declarative-agent-typespec/AGENTS.md b/templates/vsc/common/declarative-agent-typespec/AGENTS.md index eb79dc437bc..bb667e04dfe 100644 --- a/templates/vsc/common/declarative-agent-typespec/AGENTS.md +++ b/templates/vsc/common/declarative-agent-typespec/AGENTS.md @@ -55,6 +55,140 @@ applyTo: '**/*.tsp' - Expose actions as operations: `op getPolicies is PoliciesAPI.getPolicies`. - Trust the TypeSpec type system and define strong contracts for all API operations. +## Agent Decorators + +### @agent Decorator + +- **Purpose**: Indicates that a namespace represents an agent and provides its basic metadata. +- **Parameters**: + - `name` (string, localizable, required): The name of the declarative agent. Must contain at least one non-whitespace character and must be 100 characters or less. + - `description` (string, localizable, required): The description of the declarative agent. Must contain at least one non-whitespace character and must be 1,000 characters or less. + - `id` (string, optional): The unique identifier of the agent. +- **Usage**: + + ```typespec + @agent("Policy Assistant", "An agent that helps users find and understand company policies") + namespace PolicyAgent { + // Agent implementation + } + ``` + +- **Best practices**: + - Use clear, descriptive names that convey the agent's purpose. + - Write concise but informative descriptions that help users understand when to use the agent. + - Only specify `id` when you need to maintain a specific agent identifier across deployments. + +### @instructions Decorator + +- **Purpose**: Defines the instructions that guide the agent's behavior. +- **Parameters**: + - `instructions` (string, not localizable, required): The detailed instructions or guidelines on how the declarative agent should behave, its functions, and any behaviors to avoid. Must contain at least one non-whitespace character and must be 8,000 characters or less. +- **Usage**: + + ```typespec + @instructions(Prompts.INSTRUCTIONS) + namespace PolicyAgent { + // Agent implementation + } + ``` + +- **Best practices**: + - Store instructions in a dedicated `Prompts` namespace in `instructions.tsp`. + - Use clear, directive language with keywords like **ALWAYS**, **NEVER**, **MUST**. + - Provide concrete examples of expected behavior. + - Keep instructions focused and relevant to the agent's purpose. + +### @conversationStarter Decorator + +- **Purpose**: Defines conversation starters that guide users on how to interact with the agent. +- **Parameters**: + - `conversationStarter` (ConversationStarter object, required): An object with `title` (optional) and `text` (required) properties. +- **ConversationStarter Model**: + - `title` (string, localizable, optional): A unique title for the conversation starter. Must contain at least one non-whitespace character. + - `text` (string, localizable, required): A suggestion that the user can use to obtain the desired result. Must contain at least one non-whitespace character. +- **Usage**: + + ```typespec + @conversationStarter(#{ title: "Find Policies", text: "Show me all compliance policies" }) + @conversationStarter(#{ text: "What's the vacation policy?" }) + namespace PolicyAgent { + // Agent implementation + } + ``` + +- **Best practices**: + - Limit the number of conversation starters to 12. + - Use conversation starters to showcase the agent's key capabilities. + - Make suggestions specific and actionable. + - Cover diverse use cases to help users understand the agent's range. + +### @disclaimer Decorator + +- **Purpose**: Displays a disclaimer message to users at the start of a conversation to satisfy legal or compliance requirements. +- **Parameters**: + - `disclaimer` (Disclaimer object, required): An object with a `text` property. +- **Disclaimer Model**: + - `text` (string, required): The disclaimer message. Must contain at least 1 non-whitespace character. Characters beyond 500 may be ignored. +- **Usage**: + + ```typespec + @disclaimer(#{ text: "This agent provides general information only. Consult HR for official policy interpretations." }) + namespace PolicyAgent { + // Agent implementation + } + ``` + +- **Best practices**: + - Use disclaimers when the agent handles sensitive information or provides advice. + - Keep disclaimers concise and under 500 characters. + - Ensure disclaimers comply with your organization's legal and compliance requirements. + - Review disclaimers with legal/compliance teams before deployment. + +### @behaviorOverrides Decorator + +- **Purpose**: Defines settings that modify the behavior of the agent orchestration. +- **Parameters**: + - `behaviorOverrides` (BehaviorOverrides object, required): An object with optional properties to control agent behavior. +- **BehaviorOverrides Model**: + - `discourageModelKnowledge` (boolean, optional): Indicates whether the declarative agent should be discouraged from using model knowledge when generating responses. + - `disableSuggestions` (boolean, optional): Indicates whether the suggestions feature is disabled. +- **Usage**: + + ```typespec + @behaviorOverrides(#{ discourageModelKnowledge: true, disableSuggestions: false }) + namespace PolicyAgent { + // Agent implementation + } + ``` + +- **Best practices**: + - Use `discourageModelKnowledge: true` when you want the agent to rely primarily on provided knowledge sources rather than pre-trained knowledge. + - Use `disableSuggestions: false` (default) to keep the suggestions feature enabled for better user experience. + - Test behavior overrides thoroughly to ensure they produce the desired agent behavior. + - Document the reasons for using behavior overrides in your agent documentation. + +### @customExtension Decorator + +- **Purpose**: Allows adding custom extension properties to the agent for future extensibility. +- **Parameters**: + - `key` (string, required): The key name for the custom extension property. + - `value` (unknown, required): The value for the custom extension property (can be any type). +- **Usage**: + + ```typespec + @customExtension("customProperty", "customValue") + @customExtension("featureFlag", true) + namespace PolicyAgent { + // Agent implementation + } + ``` + +- **Best practices**: + - Use custom extensions sparingly and only when needed for specific scenarios. + - Document the purpose and expected values of custom extensions clearly. + - Coordinate with Microsoft documentation or support to understand supported custom extensions. + - Be prepared for custom extensions to be ignored if not recognized by the platform. + ## Action Definitions - Define actions in dedicated namespaces decorated with `@service` and `@server(url)`. @@ -166,7 +300,7 @@ Microsoft 365 Copilot agents support multiple capabilities that extend the agent - **Example (scoped to Microsoft Learn)**: ```typescript - op webSearch is AgentCapabilities.WebSearch ``` @@ -343,7 +477,7 @@ Microsoft 365 Copilot agents support multiple capabilities that extend the agent - **Enable (unscoped)**: `op graphConnectors is AgentCapabilities.CopilotConnectors` - **Enable (scoped)**: - ```typescript + ```typespec op copilotConnectors is AgentCapabilities.CopilotConnectors ``` @@ -392,10 +526,117 @@ Microsoft 365 Copilot agents support multiple capabilities that extend the agent - **Example instruction**: "When displaying usage trends, use CodeInterpreter to create a line chart with dates on x-axis and usage count on y-axis. Always include a descriptive title." - **Example**: - ```typescript + ```typespec op codeInterpreter is AgentCapabilities.CodeInterpreter; ``` +### Meetings Capability + +- **Enable**: `op meetings is AgentCapabilities.Meetings` +- **Purpose**: Allows the agent to search meeting content. +- **Scoping**: + - **NOTE**: This capability does not support scoping parameters. + - The agent can search through meeting content that the user has access to. +- **Best practices**: + - Use when your agent needs to access information from meeting transcripts, recordings, or notes. + - Respect user permissions — the agent can only access meetings the user can see. + - Guide the agent to cite meeting titles, dates, and participants when referencing meeting content. + - Consider combining with People capability to provide context about meeting participants. + - Be aware of privacy and confidentiality when using meeting content as knowledge. +- **Example instruction**: "When the user asks about decisions made in meetings, search meeting content for relevant discussions. Include the meeting date and key participants." +- **Example**: + + ```typespec + op meetings is AgentCapabilities.Meetings; + ``` + +### ScenarioModels Capability + +- **Enable**: + + ```typespec + op scenarioModels is AgentCapabilities.ScenarioModels + ``` + +- **Purpose**: Allows the agent to use task-specific models for specialized scenarios. +- **Scoping**: + - **CRITICAL**: The `Models` parameter is **required** and must contain at least one model ID. + - Specify `Models` parameter with model IDs to enable specific task-specific models. + - Model IDs are provided by Microsoft and identify specialized AI models for specific tasks. +- **Best practices**: + - Use when your agent requires specialized models for domain-specific tasks. + - Ensure you have the appropriate licenses and permissions for the models you specify. + - Document which models are being used and their purpose in your agent documentation. + - Test thoroughly with the specific models to ensure expected behavior. + - Monitor model availability and updates from Microsoft. +- **Example instruction**: "Use specialized models for domain-specific analysis when processing user queries." +- **Example**: + + ```typespec + op scenarioModels is AgentCapabilities.ScenarioModels + ``` + +### Dataverse Capability + +- **Enable (unscoped)**: `op dataverse is AgentCapabilities.Dataverse` +- **Enable (scoped)**: + + ```typespec + op dataverse is AgentCapabilities.Dataverse + ``` + +- **Enable (scoped with tables)**: + + ```typespec + op dataverse is AgentCapabilities.Dataverse + ``` + +- **Purpose**: Allows the agent to search for information in Microsoft Dataverse. +- **Scoping**: + - **CRITICAL**: Scoping is done via the `KnowledgeSources` generic parameter in the capability definition, **NOT** in instructions. + - **NOT scoped**: Omit `KnowledgeSources` parameter to allow access to all accessible Dataverse environments. + - **Scoped by environment**: Specify `KnowledgeSources` parameter with environment hostnames. + - **Scoped by tables**: Include `tables` array within a knowledge source to limit to specific Dataverse tables. + - **Scoped by skill**: Include `skill` property to use a specific skill identifier. +- **Best practices**: + - Use when your agent needs to access business data stored in Dataverse. + - Scope to specific environments and tables when you want to limit access to relevant data. + - Respect user permissions — the agent can only access Dataverse data the user has permission to view. + - Document the Dataverse schema and table relationships in your agent documentation. + - Guide the agent on how to interpret Dataverse entity relationships and field meanings. + - Consider combining with other capabilities for richer experiences (e.g., Dataverse + CodeInterpreter for data visualization). + - Test with real Dataverse data to ensure proper query formation and result interpretation. +- **Example instruction**: "When the user asks about customer information, search Dataverse for account and contact records. Provide relevant details while respecting data privacy." +- **Example (scoped to specific environment and tables)**: + + ```typespec + op dataverse is AgentCapabilities.Dataverse + ``` + ### Combining Capabilities - **Multi-modal experiences**: Combine capabilities to create powerful workflows. From 96a4c72dc2654e3a974a7b53599b0b4222811883 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Levert?= Date: Thu, 23 Oct 2025 22:05:43 -0400 Subject: [PATCH 11/24] feat: add auto-generated environment variables file for declarative agent --- .../vsc/common/declarative-agent-typespec/src/agent/env.tsp | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 templates/vsc/common/declarative-agent-typespec/src/agent/env.tsp diff --git a/templates/vsc/common/declarative-agent-typespec/src/agent/env.tsp b/templates/vsc/common/declarative-agent-typespec/src/agent/env.tsp new file mode 100644 index 00000000000..323a37cbeb8 --- /dev/null +++ b/templates/vsc/common/declarative-agent-typespec/src/agent/env.tsp @@ -0,0 +1,6 @@ +// This file is auto-generated by scripts/generate-env.js +// Run 'npm run generate:env' to regenerate this file from environment variables + +namespace Environment { + // Environment variables will be defined here +} From 97292beaa6a1af560060a9ceb20f62d33d490424 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Levert?= Date: Fri, 31 Oct 2025 09:45:45 -0400 Subject: [PATCH 12/24] fix: update @microsoft/typespec-m365-copilot version to 1.0.0-rc.6 --- .../vsc/common/declarative-agent-typespec/package.json.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/vsc/common/declarative-agent-typespec/package.json.tpl b/templates/vsc/common/declarative-agent-typespec/package.json.tpl index 8b4af243969..c139e895e82 100644 --- a/templates/vsc/common/declarative-agent-typespec/package.json.tpl +++ b/templates/vsc/common/declarative-agent-typespec/package.json.tpl @@ -6,7 +6,7 @@ "generate:env": "node scripts/generate-env.js" }, "devDependencies": { - "@microsoft/typespec-m365-copilot": "1.0.0-rc.5", + "@microsoft/typespec-m365-copilot": "1.0.0-rc.6", "@typespec/compiler": "^1.0.0", "@typespec/http": "^1.0.0", "@typespec/openapi": "^1.0.0", From 70bd2e2ec0068bd4b5823a64882b0be27ff0743a Mon Sep 17 00:00:00 2001 From: Yuqi Zhou <86260893+yuqizhou77@users.noreply.github.com> Date: Tue, 23 Sep 2025 14:58:01 +0800 Subject: [PATCH 13/24] feat: Teams AI CSharp batch 3 (#14579) * refactor: template * refactor: template * refactor: template * refactor: more * refactor: more --- .../component/generator/openApiSpec/helper.ts | 116 +++++++++++------- .../Controllers/Controller.cs.tpl | 2 +- .../custom-copilot-basic/Program.cs.tpl | 5 + .../Utils/TextUtils.cs.tpl | 2 +- .../appsettings.Playground.json.tpl | 2 +- .../custom-copilot-basic/appsettings.json.tpl | 2 +- .../{{ProjectName}}.csproj.tpl | 1 + .../Controllers/Controller.cs.tpl | 1 - .../Program.cs.tpl | 5 + .../appsettings.Playground.json.tpl | 2 +- .../appsettings.json.tpl | 2 +- .../{{ProjectName}}.csproj.tpl | 1 + .../Controllers/Controller.cs.tpl | 1 - .../Program.cs.tpl | 5 + .../appsettings.Playground.json.tpl | 2 +- .../appsettings.json.tpl | 2 +- .../{{ProjectName}}.csproj.tpl | 1 + .../default-bot/Controllers/Controller.cs.tpl | 1 - .../default-bot/appsettings.Playground.json | 2 +- .../vs/csharp/default-bot/appsettings.json | 2 +- .../appsettings.Playground.json | 2 +- .../APIActions.cs.tpl | 65 ---------- .../APIBot.cs.tpl | 15 --- .../Controllers/Controller.cs.tpl | 20 +-- .../Functions/FunctionDefinitionLoader.cs.tpl | 17 +++ .../Functions/Handlers.cs.tpl | 43 +++++++ .../Functions/functions.json | 0 .../Models/{State.tpl => State.cs.tpl} | 0 .../Program.cs.tpl | 14 +++ .../appsettings.Playground.json.tpl | 2 +- .../appsettings.json.tpl | 2 +- .../{{ProjectName}}.csproj.tpl | 4 +- 32 files changed, 195 insertions(+), 146 deletions(-) delete mode 100644 templates/vs/csharp/teams-agent-with-data-custom-api-v2/APIActions.cs.tpl delete mode 100644 templates/vs/csharp/teams-agent-with-data-custom-api-v2/APIBot.cs.tpl create mode 100644 templates/vs/csharp/teams-agent-with-data-custom-api-v2/Functions/FunctionDefinitionLoader.cs.tpl create mode 100644 templates/vs/csharp/teams-agent-with-data-custom-api-v2/Functions/Handlers.cs.tpl create mode 100644 templates/vs/csharp/teams-agent-with-data-custom-api-v2/Functions/functions.json rename templates/vs/csharp/teams-agent-with-data-custom-api-v2/Models/{State.tpl => State.cs.tpl} (100%) diff --git a/packages/fx-core/src/component/generator/openApiSpec/helper.ts b/packages/fx-core/src/component/generator/openApiSpec/helper.ts index ae1c523c454..c4864d4afe2 100644 --- a/packages/fx-core/src/component/generator/openApiSpec/helper.ts +++ b/packages/fx-core/src/component/generator/openApiSpec/helper.ts @@ -1203,14 +1203,14 @@ async function updatePromptForCustomApi( ): Promise { if (commonLanguages.includes(language as ProgrammingLanguage)) { const object = `{ "path": null, "body": null, "query": null }`; - const cSharpObject = `{ "path": {}, "body": {}, "query": {} }`; + const cSharpObject = `{ "path": {}, "query": {} }`; const promptFilePath = path.join(chatFolder, promptFileName); const prompt = `The following is a conversation with an AI assistant.\nThe assistant can help to call APIs for the open api spec file${ spec.info.description ? ". " + spec.info.description : "." }\nIf the API doesn't require parameters, invoke it with default JSON object ${ (language as ProgrammingLanguage) === ProgrammingLanguage.CSharp ? cSharpObject : object }.\n\n${ - shouldGenerateTeamsAIV2Code(language) ? "context:\nAvailable actions: {{getAction}}." : "" + shouldGenerateTeamsAIV2Code(language) ? "" : "context:\nAvailable actions: {{getAction}}." }`; await fs.writeFile(promptFilePath, prompt, { encoding: "utf-8", flag: "w" }); } @@ -1306,7 +1306,11 @@ function filterSchema(schema: OpenAPIV3.SchemaObject): OpenAPIV3.SchemaObject { } function shouldGenerateTeamsAIV2Code(language: string) { - return language === ProgrammingLanguage.JS || language === ProgrammingLanguage.TS; + return ( + language === ProgrammingLanguage.JS || + language === ProgrammingLanguage.TS || + language === ProgrammingLanguage.CSharp + ); } async function updateActionForCustomApi( @@ -1442,33 +1446,6 @@ async def {{operationId}}( await context.send_activity(message) return "success" `, - cs: ` - [Action("{{operationId}}")] - public async Task {{functionName}}Async([ActionTurnContext] ITurnContext turnContext, [ActionTurnState] TurnState turnState, [ActionParameters] Dictionary args) - { - try - { - RequestParams requestParam = ParseRequestParams(args); - - var response = await Client.CallAsync("{{apiPath}}", Method.{{apiMethod}}, requestParam); - var data = response.Content; - - var cardTemplatePath = "./adaptiveCards/{{operationId}}.json"; - if (File.Exists(cardTemplatePath)) { - var message = RenderCardToMessage(cardTemplatePath, data); - await turnContext.SendActivityAsync(message); - } - else - { - await turnContext.SendActivityAsync(data); - } - } - catch (Exception ex) { - await turnContext.SendActivityAsync("Failed to call API with error: " + ex.Message); - } - - return "complete"; - }`, }; const functionDefinitionCode = { @@ -1500,6 +1477,12 @@ const functionDefinitionCode = { } } )`, + cs: ` + var {{operationId}}SchemaJson = FunctionDefinitionLoader.FunctionDefinitions["{{operationId}}"]["parameters"].ToJsonString(); + JsonSchema {{operationId}}Schema = JsonSchema.FromText({{operationId}}SchemaJson); + prompt.Functions.Add(new Function(FunctionDefinitionLoader.FunctionDefinitions["{{operationId}}"]["name"].ToString(), + FunctionDefinitionLoader.FunctionDefinitions["{{operationId}}"]["description"].ToString(), + {{operationId}}Schema, (IDictionary args) => handlers.{{functionName}}(args).GetAwaiter().GetResult()));`, }; const functionHandlerCode = { @@ -1557,6 +1540,33 @@ module.exports = { {{operationId}}Handler };`, } };`, + cs: ` + public async Task {{functionName}}(IDictionary args) + { + try + { + RequestParams requestParam = ParseRequestParams(args); + var response = await Client.CallAsync("{{apiPath}}", Method.{{apiMethod}}, requestParam); + var data = response.Content; + + var cardTemplatePath = "./adaptiveCards/{{operationId}}.json"; + if (File.Exists(cardTemplatePath)) { + var card = RenderCard(cardTemplatePath, data); + await context.Send(new MessageActivity().AddAttachment(System.Text.Json.JsonSerializer.Deserialize(card))); + } + else + { + await context.Send(data); + } + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + + return "results are shown already. completed."; + } +`, }; const AuthCode = { @@ -1639,23 +1649,47 @@ async function updateCodeForCustomApi( .replace("# Replace with action code", actionsCode.join("\n")); await fs.writeFile(botFilePath, updateBotFileContent); } else if (language === ProgrammingLanguage.CSharp) { - const actionsCode = []; - const codeTemplate = ActionCode["cs"]; + const functionDefinitionsCode = []; + const functionHandlersCode = []; + const functionDefinitionTemplate = functionDefinitionCode["cs"]; + const functionHandlerTemplate = functionHandlerCode["cs"]; for (const item of specItems) { - const code = codeTemplate + functionDefinitionsCode.push( + functionDefinitionTemplate + .replace(/{{operationId}}/g, item.item.operationId!) + .replace(/{{functionName}}/g, Utils.updateFirstLetter(item.item.operationId!)) + ); + const code = functionHandlerTemplate .replace(/{{operationId}}/g, item.item.operationId!) .replace(/{{apiPath}}/g, item.pathUrl) .replace(/{{apiMethod}}/g, Utils.updateFirstLetter(item.method)) .replace(/{{functionName}}/g, Utils.updateFirstLetter(item.item.operationId!)); - actionsCode.push(code); + functionHandlersCode.push(code); } - const apiActionCsFilePath = path.join(destinationPath, "APIActions.cs"); - const apiActionCsFileContent = (await fs.readFile(apiActionCsFilePath)).toString(); - const updateApiActionCsFileContent = apiActionCsFileContent - .replace("{{OPENAPI_SPEC_PATH}}", "apiSpecificationFile/" + openapiSpecFileName) - .replace("// Replace with action code", actionsCode.join("\n")); - await fs.writeFile(apiActionCsFilePath, updateApiActionCsFileContent); + const controllerFilePath = path.join(destinationPath, "Controllers", "Controller.cs"); + const controllerFileContent = (await fs.readFile(controllerFilePath)).toString(); + const updatedControllerFileContent = controllerFileContent.replace( + "// Replace with function definition code", + `${functionDefinitionsCode.join("\n")};` + ); + await fs.writeFile(controllerFilePath, updatedControllerFileContent); + + const handlerFilePath = path.join(destinationPath, "Functions", "Handlers.cs"); + const handlerFileContent = (await fs.readFile(handlerFilePath)).toString(); + const updatedHandlerFileContent = handlerFileContent.replace( + "// Replace with function handler code", + `${functionHandlersCode.join("\n")}` + ); + await fs.writeFile(handlerFilePath, updatedHandlerFileContent); + + const startFilePath = path.join(destinationPath, "Program.cs"); + const startFileContent = (await fs.readFile(startFilePath)).toString(); + const updatedStartFileContent = startFileContent.replace( + "{{OPENAPI_SPEC_PATH}}", + "apiSpecificationFile/" + openapiSpecFileName + ); + await fs.writeFile(startFilePath, updatedStartFileContent); const files = await fs.readdir(destinationPath); const projectFileName = files.find((file) => file.endsWith(".csproj")); @@ -1680,7 +1714,7 @@ export async function updateForCustomApi( ? path.join(destinationPath, "src", "app") : path.join(destinationPath, "src", "prompts", "chat"); if (language === ProgrammingLanguage.CSharp) { - chatFolder = path.join(destinationPath, "prompts", "Chat"); + chatFolder = path.join(destinationPath, "Functions"); } await fs.ensureDir(chatFolder); diff --git a/templates/vs/csharp/custom-copilot-basic/Controllers/Controller.cs.tpl b/templates/vs/csharp/custom-copilot-basic/Controllers/Controller.cs.tpl index 948ea58dff0..31a55e0ba27 100644 --- a/templates/vs/csharp/custom-copilot-basic/Controllers/Controller.cs.tpl +++ b/templates/vs/csharp/custom-copilot-basic/Controllers/Controller.cs.tpl @@ -1,4 +1,5 @@ using {{SafeProjectName}}.Models; +using {{SafeProjectName}}.Utils; using Microsoft.Teams.AI.Models.OpenAI; using Microsoft.Teams.Api.Activities; using Microsoft.Teams.Api.Activities.Invokes; @@ -53,7 +54,6 @@ namespace {{SafeProjectName}}.Controllers await context.Send(welcomeText); } } - } } } \ No newline at end of file diff --git a/templates/vs/csharp/custom-copilot-basic/Program.cs.tpl b/templates/vs/csharp/custom-copilot-basic/Program.cs.tpl index e58a02ec838..5509ccd0086 100644 --- a/templates/vs/csharp/custom-copilot-basic/Program.cs.tpl +++ b/templates/vs/csharp/custom-copilot-basic/Program.cs.tpl @@ -14,6 +14,11 @@ using Microsoft.Teams.Plugins.AspNetCore.Extensions; var builder = WebApplication.CreateBuilder(args); var config = builder.Configuration.Get(); +if (config == null) +{ + throw new InvalidOperationException("Missing configuration for ConfigOptions"); +} + Func> createTokenFactory = async (string[] scopes, string? tenantId) => { var clientId = config.Teams.ClientId; diff --git a/templates/vs/csharp/custom-copilot-basic/Utils/TextUtils.cs.tpl b/templates/vs/csharp/custom-copilot-basic/Utils/TextUtils.cs.tpl index a5615ec32c2..0c9e18cde93 100644 --- a/templates/vs/csharp/custom-copilot-basic/Utils/TextUtils.cs.tpl +++ b/templates/vs/csharp/custom-copilot-basic/Utils/TextUtils.cs.tpl @@ -3,7 +3,7 @@ using Microsoft.Teams.Api.Entities; namespace {{SafeProjectName}}.Utils { - public static class Utils + public static class TextUtils { public static string StripMentionsText(MessageActivity activity) { diff --git a/templates/vs/csharp/custom-copilot-basic/appsettings.Playground.json.tpl b/templates/vs/csharp/custom-copilot-basic/appsettings.Playground.json.tpl index d7bde6d9373..a5599fbdc81 100644 --- a/templates/vs/csharp/custom-copilot-basic/appsettings.Playground.json.tpl +++ b/templates/vs/csharp/custom-copilot-basic/appsettings.Playground.json.tpl @@ -10,7 +10,7 @@ } }, "AllowedHosts": "*", - "Teams": { + "Teams": { "ClientId": "", "ClientSecret": "", "TenantId": "", diff --git a/templates/vs/csharp/custom-copilot-basic/appsettings.json.tpl b/templates/vs/csharp/custom-copilot-basic/appsettings.json.tpl index 45930a17eff..94d178571b3 100644 --- a/templates/vs/csharp/custom-copilot-basic/appsettings.json.tpl +++ b/templates/vs/csharp/custom-copilot-basic/appsettings.json.tpl @@ -10,7 +10,7 @@ } }, "AllowedHosts": "*", - "Teams": { + "Teams": { "ClientId": "", "ClientSecret": "", "BotType": "" diff --git a/templates/vs/csharp/custom-copilot-basic/{{ProjectName}}.csproj.tpl b/templates/vs/csharp/custom-copilot-basic/{{ProjectName}}.csproj.tpl index 7523eefea66..8619bd768e1 100644 --- a/templates/vs/csharp/custom-copilot-basic/{{ProjectName}}.csproj.tpl +++ b/templates/vs/csharp/custom-copilot-basic/{{ProjectName}}.csproj.tpl @@ -3,6 +3,7 @@ {{TargetFramework}} enable + enable {{^isNewProjectTypeEnabled}} diff --git a/templates/vs/csharp/custom-copilot-rag-azure-ai-search/Controllers/Controller.cs.tpl b/templates/vs/csharp/custom-copilot-rag-azure-ai-search/Controllers/Controller.cs.tpl index a228bce032a..c7657c101a8 100644 --- a/templates/vs/csharp/custom-copilot-rag-azure-ai-search/Controllers/Controller.cs.tpl +++ b/templates/vs/csharp/custom-copilot-rag-azure-ai-search/Controllers/Controller.cs.tpl @@ -57,7 +57,6 @@ namespace {{SafeProjectName}}.Controllers await context.Send(welcomeText); } } - } } } \ No newline at end of file diff --git a/templates/vs/csharp/custom-copilot-rag-azure-ai-search/Program.cs.tpl b/templates/vs/csharp/custom-copilot-rag-azure-ai-search/Program.cs.tpl index 02b733784d4..426c2bd07ac 100644 --- a/templates/vs/csharp/custom-copilot-rag-azure-ai-search/Program.cs.tpl +++ b/templates/vs/csharp/custom-copilot-rag-azure-ai-search/Program.cs.tpl @@ -14,6 +14,11 @@ using Microsoft.Teams.Plugins.AspNetCore.Extensions; var builder = WebApplication.CreateBuilder(args); var config = builder.Configuration.Get(); +if (config == null) +{ + throw new InvalidOperationException("Missing configuration for ConfigOptions"); +} + Func> createTokenFactory = async (string[] scopes, string? tenantId) => { var clientId = config.Teams.ClientId; diff --git a/templates/vs/csharp/custom-copilot-rag-azure-ai-search/appsettings.Playground.json.tpl b/templates/vs/csharp/custom-copilot-rag-azure-ai-search/appsettings.Playground.json.tpl index 1b0958b189d..f5ccb73caf6 100644 --- a/templates/vs/csharp/custom-copilot-rag-azure-ai-search/appsettings.Playground.json.tpl +++ b/templates/vs/csharp/custom-copilot-rag-azure-ai-search/appsettings.Playground.json.tpl @@ -10,7 +10,7 @@ } }, "AllowedHosts": "*", - "Teams": { + "Teams": { "ClientId": "", "ClientSecret": "", "BotType": "" diff --git a/templates/vs/csharp/custom-copilot-rag-azure-ai-search/appsettings.json.tpl b/templates/vs/csharp/custom-copilot-rag-azure-ai-search/appsettings.json.tpl index cabb4d6bd30..07a7bb5a53a 100644 --- a/templates/vs/csharp/custom-copilot-rag-azure-ai-search/appsettings.json.tpl +++ b/templates/vs/csharp/custom-copilot-rag-azure-ai-search/appsettings.json.tpl @@ -10,7 +10,7 @@ } }, "AllowedHosts": "*", - "Teams": { + "Teams": { "ClientId": "", "ClientSecret": "", "BotType": "" diff --git a/templates/vs/csharp/custom-copilot-rag-azure-ai-search/{{ProjectName}}.csproj.tpl b/templates/vs/csharp/custom-copilot-rag-azure-ai-search/{{ProjectName}}.csproj.tpl index e81802f524b..365e0f1f925 100644 --- a/templates/vs/csharp/custom-copilot-rag-azure-ai-search/{{ProjectName}}.csproj.tpl +++ b/templates/vs/csharp/custom-copilot-rag-azure-ai-search/{{ProjectName}}.csproj.tpl @@ -3,6 +3,7 @@ {{TargetFramework}} enable + enable {{^isNewProjectTypeEnabled}} diff --git a/templates/vs/csharp/custom-copilot-rag-customize/Controllers/Controller.cs.tpl b/templates/vs/csharp/custom-copilot-rag-customize/Controllers/Controller.cs.tpl index 68b8c1c8907..166c019297a 100644 --- a/templates/vs/csharp/custom-copilot-rag-customize/Controllers/Controller.cs.tpl +++ b/templates/vs/csharp/custom-copilot-rag-customize/Controllers/Controller.cs.tpl @@ -57,7 +57,6 @@ namespace {{SafeProjectName}}.Controllers await context.Send(welcomeText); } } - } } } \ No newline at end of file diff --git a/templates/vs/csharp/custom-copilot-rag-customize/Program.cs.tpl b/templates/vs/csharp/custom-copilot-rag-customize/Program.cs.tpl index 2dda929956c..57453ac580a 100644 --- a/templates/vs/csharp/custom-copilot-rag-customize/Program.cs.tpl +++ b/templates/vs/csharp/custom-copilot-rag-customize/Program.cs.tpl @@ -14,6 +14,11 @@ using Microsoft.Teams.Plugins.AspNetCore.Extensions; var builder = WebApplication.CreateBuilder(args); var config = builder.Configuration.Get(); +if (config == null) +{ + throw new InvalidOperationException("Missing configuration for ConfigOptions"); +} + Func> createTokenFactory = async (string[] scopes, string? tenantId) => { var clientId = config.Teams.ClientId; diff --git a/templates/vs/csharp/custom-copilot-rag-customize/appsettings.Playground.json.tpl b/templates/vs/csharp/custom-copilot-rag-customize/appsettings.Playground.json.tpl index d7bde6d9373..a5599fbdc81 100644 --- a/templates/vs/csharp/custom-copilot-rag-customize/appsettings.Playground.json.tpl +++ b/templates/vs/csharp/custom-copilot-rag-customize/appsettings.Playground.json.tpl @@ -10,7 +10,7 @@ } }, "AllowedHosts": "*", - "Teams": { + "Teams": { "ClientId": "", "ClientSecret": "", "TenantId": "", diff --git a/templates/vs/csharp/custom-copilot-rag-customize/appsettings.json.tpl b/templates/vs/csharp/custom-copilot-rag-customize/appsettings.json.tpl index 45930a17eff..94d178571b3 100644 --- a/templates/vs/csharp/custom-copilot-rag-customize/appsettings.json.tpl +++ b/templates/vs/csharp/custom-copilot-rag-customize/appsettings.json.tpl @@ -10,7 +10,7 @@ } }, "AllowedHosts": "*", - "Teams": { + "Teams": { "ClientId": "", "ClientSecret": "", "BotType": "" diff --git a/templates/vs/csharp/custom-copilot-rag-customize/{{ProjectName}}.csproj.tpl b/templates/vs/csharp/custom-copilot-rag-customize/{{ProjectName}}.csproj.tpl index e6de8ec6c83..b100733678f 100644 --- a/templates/vs/csharp/custom-copilot-rag-customize/{{ProjectName}}.csproj.tpl +++ b/templates/vs/csharp/custom-copilot-rag-customize/{{ProjectName}}.csproj.tpl @@ -3,6 +3,7 @@ {{TargetFramework}} enable + enable {{^isNewProjectTypeEnabled}} diff --git a/templates/vs/csharp/default-bot/Controllers/Controller.cs.tpl b/templates/vs/csharp/default-bot/Controllers/Controller.cs.tpl index e4af07df18d..5840472a0ed 100644 --- a/templates/vs/csharp/default-bot/Controllers/Controller.cs.tpl +++ b/templates/vs/csharp/default-bot/Controllers/Controller.cs.tpl @@ -27,7 +27,6 @@ namespace {{SafeProjectName}}.Controllers await context.Send(welcomeText); } } - } } } \ No newline at end of file diff --git a/templates/vs/csharp/default-bot/appsettings.Playground.json b/templates/vs/csharp/default-bot/appsettings.Playground.json index a1d7d28f4af..0497106b3c0 100644 --- a/templates/vs/csharp/default-bot/appsettings.Playground.json +++ b/templates/vs/csharp/default-bot/appsettings.Playground.json @@ -10,7 +10,7 @@ } }, "AllowedHosts": "*", - "Teams": { + "Teams": { "ClientId": "", "ClientSecret": "", "TenantId": "", diff --git a/templates/vs/csharp/default-bot/appsettings.json b/templates/vs/csharp/default-bot/appsettings.json index 15b75d33ed5..9e3379db53f 100644 --- a/templates/vs/csharp/default-bot/appsettings.json +++ b/templates/vs/csharp/default-bot/appsettings.json @@ -10,7 +10,7 @@ } }, "AllowedHosts": "*", - "Teams": { + "Teams": { "ClientId": "", "ClientSecret": "", "BotType": "" diff --git a/templates/vs/csharp/message-extension-v2/appsettings.Playground.json b/templates/vs/csharp/message-extension-v2/appsettings.Playground.json index a1d7d28f4af..0497106b3c0 100644 --- a/templates/vs/csharp/message-extension-v2/appsettings.Playground.json +++ b/templates/vs/csharp/message-extension-v2/appsettings.Playground.json @@ -10,7 +10,7 @@ } }, "AllowedHosts": "*", - "Teams": { + "Teams": { "ClientId": "", "ClientSecret": "", "TenantId": "", diff --git a/templates/vs/csharp/teams-agent-with-data-custom-api-v2/APIActions.cs.tpl b/templates/vs/csharp/teams-agent-with-data-custom-api-v2/APIActions.cs.tpl deleted file mode 100644 index 799ac52e67e..00000000000 --- a/templates/vs/csharp/teams-agent-with-data-custom-api-v2/APIActions.cs.tpl +++ /dev/null @@ -1,65 +0,0 @@ -using AdaptiveCards.Templating; -using AdaptiveCards; -using Microsoft.Bot.Builder; -using Microsoft.Teams.AI.AI.Action; -using Microsoft.Teams.AI.AI; -using Microsoft.Teams.AI.State; -using Newtonsoft.Json.Linq; -using Microsoft.Bot.Schema; -using RestSharp; -using OpenAPIClient; - -namespace {{SafeProjectName}} -{ - public class APIActions - { - private APIClient Client; - - public APIActions() - { - Client = new APIClient("{{OPENAPI_SPEC_PATH}}"); - } - - // Replace with action code - - private static IMessageActivity RenderCardToMessage(string cardTemplatePath, string data) - { - try - { - var templateString = File.ReadAllText(cardTemplatePath); - AdaptiveCardTemplate template = new AdaptiveCardTemplate(templateString); - var cardBody = template.Expand(data); - - Attachment attachment = new Attachment() - { - ContentType = AdaptiveCard.ContentType, - Content = JObject.Parse(cardBody) - }; - - return MessageFactory.Attachment(attachment); - } - catch (Exception ex) { - throw new Exception("Failed to render adaptive card: " + ex.Message); - } - } - - private static RequestParams ParseRequestParams(Dictionary args) - { - RequestParams requestParam = new RequestParams - { - PathObject = args.ContainsKey("path") ? args["path"] : null, - HeaderObject = args.ContainsKey("header") ? args["header"] : null, - QueryObject = args.ContainsKey("query") ? args["query"] : null, - RequestBody = args.ContainsKey("body") ? args["body"] : null - }; - return requestParam; - } - - [Action(AIConstants.UnknownActionName)] - public async Task UnknownActionAsync([ActionTurnContext] TurnContext turnContext, [ActionName] string action) - { - await turnContext.SendActivityAsync(MessageFactory.Text("Unable to find a matching API to call")); - return "unknown action"; - } - } -} diff --git a/templates/vs/csharp/teams-agent-with-data-custom-api-v2/APIBot.cs.tpl b/templates/vs/csharp/teams-agent-with-data-custom-api-v2/APIBot.cs.tpl deleted file mode 100644 index 568cc63ba16..00000000000 --- a/templates/vs/csharp/teams-agent-with-data-custom-api-v2/APIBot.cs.tpl +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.Teams.AI.State; -using Microsoft.Teams.AI; -using {{SafeProjectName}}.Models; - -namespace {{SafeProjectName}} -{ - public class APIBot : Application - { - public APIBot(ApplicationOptions options) : base(options) - { - // Registering action handlers that will be hooked up to the planner. - AI.ImportActions(new APIActions()); - } - } -} \ No newline at end of file diff --git a/templates/vs/csharp/teams-agent-with-data-custom-api-v2/Controllers/Controller.cs.tpl b/templates/vs/csharp/teams-agent-with-data-custom-api-v2/Controllers/Controller.cs.tpl index 49591e0ff4e..270b9fb7165 100644 --- a/templates/vs/csharp/teams-agent-with-data-custom-api-v2/Controllers/Controller.cs.tpl +++ b/templates/vs/csharp/teams-agent-with-data-custom-api-v2/Controllers/Controller.cs.tpl @@ -1,4 +1,5 @@ -using {{SafeProjectName}}.Models; +using {{SafeProjectName}}.Functions; +using {{SafeProjectName}}.Models; using Microsoft.Teams.AI.Models.OpenAI; using Microsoft.Teams.Api.Activities; using Microsoft.Teams.Api.Activities.Invokes; @@ -6,26 +7,31 @@ using Microsoft.Teams.Apps; using Microsoft.Teams.Apps.Activities; using Microsoft.Teams.Apps.Activities.Invokes; using Microsoft.Teams.Apps.Annotations; +using Microsoft.Teams.AI; +using Json.Schema; namespace {{SafeProjectName}}.Controllers { [TeamsController] - public class Controller(OpenAIChatPrompt _prompt) + public class Controller(OpenAIChatPrompt prompt, Handlers handlers) { [Message] - public async Task OnMessage(IContext context) + public async Task OnMessage(IContext context, [Context] IContext.Client client) { var state = State.From(context); - var text = context.Activity.text; + + var text = context.Activity.Text; + + // Replace with function definition code + if (context.Activity.Conversation.IsGroup == true) { - var response = await _prompt.Send(text, new() { Messages = state.Messages }, null, context.CancellationToken); + var response = await prompt.Send(text, new() { Messages = state.Messages }, null, context.CancellationToken); await context.Send(new Microsoft.Teams.Api.Activities.MessageActivity(response.Content).AddFeedback().AddAIGenerated()); } else { - - await _prompt.Send(text, new() { Messages = state.Messages }, (chunk) => Task.Run(() => + await prompt.Send(text, new() { Messages = state.Messages }, (chunk) => Task.Run(() => { context.Stream.Emit(chunk); }), context.CancellationToken); diff --git a/templates/vs/csharp/teams-agent-with-data-custom-api-v2/Functions/FunctionDefinitionLoader.cs.tpl b/templates/vs/csharp/teams-agent-with-data-custom-api-v2/Functions/FunctionDefinitionLoader.cs.tpl new file mode 100644 index 00000000000..efb116d54ff --- /dev/null +++ b/templates/vs/csharp/teams-agent-with-data-custom-api-v2/Functions/FunctionDefinitionLoader.cs.tpl @@ -0,0 +1,17 @@ +using System.Text.Json.Nodes; +using System.Text.Json; + +namespace {{SafeProjectName}}.Functions +{ + public static class FunctionDefinitionLoader + { + + public static readonly JsonObject FunctionDefinitions = new JsonObject(); + + static FunctionDefinitionLoader() + { + string json = File.ReadAllText(Path.Combine("Functions","./functions.json")); + FunctionDefinitions = JsonSerializer.Deserialize(json); + } + } +} diff --git a/templates/vs/csharp/teams-agent-with-data-custom-api-v2/Functions/Handlers.cs.tpl b/templates/vs/csharp/teams-agent-with-data-custom-api-v2/Functions/Handlers.cs.tpl new file mode 100644 index 00000000000..242ebdc4ee6 --- /dev/null +++ b/templates/vs/csharp/teams-agent-with-data-custom-api-v2/Functions/Handlers.cs.tpl @@ -0,0 +1,43 @@ +using AdaptiveCards.Templating; +using Microsoft.Teams.Apps; +using Microsoft.Teams.Api.Activities; +using OpenAPIClient; +using RestSharp; + +namespace {{SafeProjectName}}.Functions +{ + public class Handlers(APIClient Client, IContext.Accessor accessor) + { + private IContext context => accessor.Value!; + + // Replace with function handler code + + private static RequestParams ParseRequestParams(IDictionary args) + { + RequestParams requestParam = new RequestParams + { + PathObject = args.ContainsKey("path") ? args["path"] : null, + HeaderObject = args.ContainsKey("header") ? args["header"] : null, + QueryObject = args.ContainsKey("query") ? args["query"] : null, + RequestBody = args.ContainsKey("body") ? args["body"] : null + }; + return requestParam; + } + + private static string RenderCard(string cardTemplatePath, string data) + { + try + { + var templateString = File.ReadAllText(cardTemplatePath); + AdaptiveCardTemplate template = new AdaptiveCardTemplate(templateString); + return template.Expand(data); + + + } + catch (Exception ex) + { + throw new Exception("Failed to render adaptive card: " + ex.Message); + } + } + } +} diff --git a/templates/vs/csharp/teams-agent-with-data-custom-api-v2/Functions/functions.json b/templates/vs/csharp/teams-agent-with-data-custom-api-v2/Functions/functions.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/templates/vs/csharp/teams-agent-with-data-custom-api-v2/Models/State.tpl b/templates/vs/csharp/teams-agent-with-data-custom-api-v2/Models/State.cs.tpl similarity index 100% rename from templates/vs/csharp/teams-agent-with-data-custom-api-v2/Models/State.tpl rename to templates/vs/csharp/teams-agent-with-data-custom-api-v2/Models/State.cs.tpl diff --git a/templates/vs/csharp/teams-agent-with-data-custom-api-v2/Program.cs.tpl b/templates/vs/csharp/teams-agent-with-data-custom-api-v2/Program.cs.tpl index e58a02ec838..daeed7948b1 100644 --- a/templates/vs/csharp/teams-agent-with-data-custom-api-v2/Program.cs.tpl +++ b/templates/vs/csharp/teams-agent-with-data-custom-api-v2/Program.cs.tpl @@ -1,5 +1,6 @@ using {{SafeProjectName}}; using {{SafeProjectName}}.Controllers; +using {{SafeProjectName}}.Functions; using Azure.Core; using Azure.Identity; using Microsoft.Teams.AI.Models.OpenAI; @@ -10,10 +11,16 @@ using Microsoft.Teams.Apps; using Microsoft.Teams.Apps.Extensions; using Microsoft.Teams.Common.Http; using Microsoft.Teams.Plugins.AspNetCore.Extensions; +using OpenAPIClient; var builder = WebApplication.CreateBuilder(args); var config = builder.Configuration.Get(); +if (config == null) +{ + throw new InvalidOperationException("Missing configuration for ConfigOptions"); +} + Func> createTokenFactory = async (string[] scopes, string? tenantId) => { var clientId = config.Teams.ClientId; @@ -41,7 +48,14 @@ if (config.Teams.BotType == "UserAssignedMsi") )); } + builder.Services.AddSingleton(); + +APIClient apiClient = new("{{OPENAPI_SPEC_PATH}}"); +builder.Services.AddSingleton(apiClient); +builder.Services.AddHttpContextAccessor(); +builder.Services.AddSingleton(); + builder.AddTeams(appBuilder); // Read instructions from file diff --git a/templates/vs/csharp/teams-agent-with-data-custom-api-v2/appsettings.Playground.json.tpl b/templates/vs/csharp/teams-agent-with-data-custom-api-v2/appsettings.Playground.json.tpl index d7bde6d9373..a5599fbdc81 100644 --- a/templates/vs/csharp/teams-agent-with-data-custom-api-v2/appsettings.Playground.json.tpl +++ b/templates/vs/csharp/teams-agent-with-data-custom-api-v2/appsettings.Playground.json.tpl @@ -10,7 +10,7 @@ } }, "AllowedHosts": "*", - "Teams": { + "Teams": { "ClientId": "", "ClientSecret": "", "TenantId": "", diff --git a/templates/vs/csharp/teams-agent-with-data-custom-api-v2/appsettings.json.tpl b/templates/vs/csharp/teams-agent-with-data-custom-api-v2/appsettings.json.tpl index 45930a17eff..94d178571b3 100644 --- a/templates/vs/csharp/teams-agent-with-data-custom-api-v2/appsettings.json.tpl +++ b/templates/vs/csharp/teams-agent-with-data-custom-api-v2/appsettings.json.tpl @@ -10,7 +10,7 @@ } }, "AllowedHosts": "*", - "Teams": { + "Teams": { "ClientId": "", "ClientSecret": "", "BotType": "" diff --git a/templates/vs/csharp/teams-agent-with-data-custom-api-v2/{{ProjectName}}.csproj.tpl b/templates/vs/csharp/teams-agent-with-data-custom-api-v2/{{ProjectName}}.csproj.tpl index 9932237ac70..6fd060f00c2 100644 --- a/templates/vs/csharp/teams-agent-with-data-custom-api-v2/{{ProjectName}}.csproj.tpl +++ b/templates/vs/csharp/teams-agent-with-data-custom-api-v2/{{ProjectName}}.csproj.tpl @@ -3,6 +3,7 @@ {{TargetFramework}} enable + enable {{^isNewProjectTypeEnabled}} @@ -26,8 +27,6 @@ - - @@ -38,6 +37,7 @@ PreserveNewest PreserveNewest + PreserveNewest From e0ef8b6e6195bf9ec48e051d2c0d69e011754458 Mon Sep 17 00:00:00 2001 From: Ning Tang Date: Tue, 23 Sep 2025 14:58:40 +0800 Subject: [PATCH 14/24] fix: template e2e cases part 2 (#14575) * fix(e2e): permission test * fix: m365 title id check * fix: add message extension test * refactor: e2e cases * fix: multi env test * refactor: clean codes * fix: update e2e workflow * fix: script * fix: bug * fix: setup bug * fix: sample env setup --- .github/actions/setup-project/action.yml | 20 +- .github/workflows/e2e-test.yml | 16 +- package.json | 3 + .../DeployOboTab.tests.bak} | 0 .../NotificationBotHappyPathCommon.bak} | 0 ...rovisionApiSpecMessageExtension.tests.bak} | 2 +- ...ppServiceNotificationBot.dotnet.tests.bak} | 0 .../incoming-webhook-notification.tests.bak} | 0 .../spfx/TestRemoteHappyPathSPFx.tests.bak} | 0 ...tAgentAssistantsApiBotForPython.tests.bak} | 8 +- .../DebugCustomCopilotAgentNewBot.tests.bak} | 8 +- ...stomCopilotAgentNewBotForPython.tests.bak} | 8 +- ...gCustomCopilotBasicBotForPython.tests.bak} | 8 +- ...mCopilotRagAiSearchBotForPython.tests.bak} | 8 +- ...stomCopilotRagBasicBotForPython.tests.bak} | 8 +- .../src/e2e/bot/CommandBotHappyPathCommon.ts | 138 ------------ .../src/e2e/bot/EchoBotHappyPath.tests.ts | 20 -- .../MessageExtensionActionHappyPath.tests.ts | 19 -- .../src/e2e/bot/WorkflowBotHappyPathCommon.ts | 115 ---------- .../debug/DebugCommandAndResponse.tests.ts | 105 ---------- .../src/e2e/debug/DebugNonSsoTab.tests.ts | 89 -------- .../DebugNotificationHttpTrigger.tests.ts | 111 ---------- .../tests/src/e2e/feature/Permission.tests.ts | 2 +- .../mosApi/appPackage-da.local.zip | Bin .../{ => feature}/mosApi/appPackage.local.zip | Bin .../{ => feature}/mosApi/sideloading.tests.ts | 4 +- .../multienv.tests.ts} | 14 +- .../m365/DebugM365MessageExtension.tests.ts | 117 ----------- .../m365/DeployM365MessageExtension.tests.ts | 92 -------- .../DebugCustomCopilotBasicBot.tests.ts | 4 +- .../DebugCustomCopilotRagAiSearchBot.tests.ts | 3 +- .../DebugCustomCopilotRagBasicBot.tests.ts | 4 +- .../tests/src/e2e/teamsApp/basicBot.tests.ts | 196 ++++++++++++++++++ .../teamsApp/basicMessageExtension.tests.ts | 196 ++++++++++++++++++ .../tests/src/e2e/teamsApp/basicTab.tests.ts | 43 +++- .../src/e2e/{bot => vs}/BotHappyPathCommon.ts | 0 .../e2e/vs/EchoBotHappyPath.dotnet.tests.ts | 2 +- 37 files changed, 492 insertions(+), 871 deletions(-) rename packages/tests/src/e2e/{m365/DeployOboTab.tests.ts => archive/DeployOboTab.tests.bak} (100%) rename packages/tests/src/e2e/{bot/NotificationBotHappyPathCommon.ts => archive/NotificationBotHappyPathCommon.bak} (100%) rename packages/tests/src/e2e/{m365/ProvisionApiSpecMessageExtension.tests.ts => archive/ProvisionApiSpecMessageExtension.tests.bak} (100%) rename packages/tests/src/e2e/{vs/ProvisionAppServiceNotificationBot.dotnet.tests.ts => archive/ProvisionAppServiceNotificationBot.dotnet.tests.bak} (100%) rename packages/tests/src/e2e/{feature/upgrade/incoming-webhook-notification.tests.ts => archive/incoming-webhook-notification.tests.bak} (100%) rename packages/tests/src/e2e/{multienv/TestRemoteHappyPathSPFx.tests.ts => archive/spfx/TestRemoteHappyPathSPFx.tests.bak} (100%) rename packages/tests/src/e2e/{debug/DebugCustomCopilotAgentAssistantsApiBotForPython.tests.ts => archive/teamsAgent/DebugCustomCopilotAgentAssistantsApiBotForPython.tests.bak} (97%) rename packages/tests/src/e2e/{debug/DebugCustomCopilotAgentNewBot.tests.ts => archive/teamsAgent/DebugCustomCopilotAgentNewBot.tests.bak} (98%) rename packages/tests/src/e2e/{debug/DebugCustomCopilotAgentNewBotForPython.tests.ts => archive/teamsAgent/DebugCustomCopilotAgentNewBotForPython.tests.bak} (97%) rename packages/tests/src/e2e/{debug/DebugCustomCopilotBasicBotForPython.tests.ts => archive/teamsAgent/DebugCustomCopilotBasicBotForPython.tests.bak} (97%) rename packages/tests/src/e2e/{debug/DebugCustomCopilotRagAiSearchBotForPython.tests.ts => archive/teamsAgent/DebugCustomCopilotRagAiSearchBotForPython.tests.bak} (97%) rename packages/tests/src/e2e/{debug/DebugCustomCopilotRagBasicBotForPython.tests.ts => archive/teamsAgent/DebugCustomCopilotRagBasicBotForPython.tests.bak} (97%) delete mode 100644 packages/tests/src/e2e/bot/CommandBotHappyPathCommon.ts delete mode 100644 packages/tests/src/e2e/bot/EchoBotHappyPath.tests.ts delete mode 100644 packages/tests/src/e2e/bot/MessageExtensionActionHappyPath.tests.ts delete mode 100644 packages/tests/src/e2e/bot/WorkflowBotHappyPathCommon.ts delete mode 100644 packages/tests/src/e2e/debug/DebugCommandAndResponse.tests.ts delete mode 100644 packages/tests/src/e2e/debug/DebugNonSsoTab.tests.ts delete mode 100644 packages/tests/src/e2e/debug/DebugNotificationHttpTrigger.tests.ts rename packages/tests/src/e2e/{ => feature}/mosApi/appPackage-da.local.zip (100%) rename packages/tests/src/e2e/{ => feature}/mosApi/appPackage.local.zip (100%) rename packages/tests/src/e2e/{ => feature}/mosApi/sideloading.tests.ts (96%) rename packages/tests/src/e2e/{multienv/TestRemoteHappyPath.tests.ts => feature/multienv.tests.ts} (98%) delete mode 100644 packages/tests/src/e2e/m365/DebugM365MessageExtension.tests.ts delete mode 100644 packages/tests/src/e2e/m365/DeployM365MessageExtension.tests.ts rename packages/tests/src/e2e/{debug => teamsAgent}/DebugCustomCopilotBasicBot.tests.ts (98%) rename packages/tests/src/e2e/{debug => teamsAgent}/DebugCustomCopilotRagAiSearchBot.tests.ts (99%) rename packages/tests/src/e2e/{debug => teamsAgent}/DebugCustomCopilotRagBasicBot.tests.ts (99%) create mode 100644 packages/tests/src/e2e/teamsApp/basicBot.tests.ts create mode 100644 packages/tests/src/e2e/teamsApp/basicMessageExtension.tests.ts rename packages/tests/src/e2e/{bot => vs}/BotHappyPathCommon.ts (100%) diff --git a/.github/actions/setup-project/action.yml b/.github/actions/setup-project/action.yml index 070b54a6125..63bb524fa70 100644 --- a/.github/actions/setup-project/action.yml +++ b/.github/actions/setup-project/action.yml @@ -5,6 +5,11 @@ inputs: description: 'setup project' required: true default: 'true' + range: # id of input + description: 'package range to install (vscode, cli, e2e, all)' + required: false + default: 'all' + runs: using: "composite" steps: @@ -22,4 +27,17 @@ runs: timeout_minutes: 10 max_attempts: 5 command: | - npm run setup \ No newline at end of file + case "${{ inputs.range }}" in + "cli") + npm run setup:cli + ;; + "vscode") + npm run setup:vsc + ;; + "e2e") + npm run setup:e2e + ;; + "all"|*) + npm run setup + ;; + esac \ No newline at end of file diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 5807fb0952d..ba4707caae5 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -78,6 +78,8 @@ jobs: - name: setup project uses: ./.github/actions/setup-project + with: + range: "e2e" - name: List cases for schedule or pull request id: schedule-cases @@ -87,7 +89,6 @@ jobs: cases=`find . -wholename "*.tests.ts" | jq -Rsc '[split("\n") | .[]| select(.!="")]'` echo "cases=$cases" >> $GITHUB_OUTPUT - - name: List cases for pull request id: pr-cases if: ${{ github.event_name == 'pull_request' }} @@ -189,19 +190,15 @@ jobs: with: node-version: 22 - - name: Setup node for SPFx project - if: contains(matrix.cases, 'SPFx') - uses: actions/setup-node@v3 - with: - node-version: 22 - - name: Setup node for Retail project if: contains(matrix.cases, 'Retail') uses: actions/setup-node@v3 with: node-version: 18 + # Azure Function is not used in templates now - name: Setup Azure Functions Core Tools For Linux + if: contains(matrix.cases, 'samples') run: | curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg sudo mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg @@ -220,10 +217,11 @@ jobs: uses: actions/setup-dotnet@v3 with: dotnet-version: | - 6.0.x 8.0.x + # Python is not used in templates now - name: Setup Python + if: contains(matrix.cases, 'samples') uses: actions/setup-python@v5 with: python-version: '3.11' @@ -232,7 +230,7 @@ jobs: - name: Setup project run: | - npm run setup + npm run setup:e2e - name: install cli with specific version if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.cli-version != '' }} diff --git a/package.json b/package.json index 419e025f8b9..e42096d29f4 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,9 @@ "main": "index.js", "scripts": { "setup": "pnpm install && npm run build", + "setup:e2e": "pnpm install && pnpm --filter templates --filter @microsoft/teamsfx-test... build", + "setup:cli": "pnpm install && pnpm --filter templates --filter @microsoft/m365agentstoolkit-cli... build", + "setup:vsc": "pnpm install && pnpm --filter templates --filter ms-teams-vscode-extension... build", "watch": "pnpm --parallel -r run watch", "build": "pnpm -r run build", "precommit": "lint-staged", diff --git a/packages/tests/src/e2e/m365/DeployOboTab.tests.ts b/packages/tests/src/e2e/archive/DeployOboTab.tests.bak similarity index 100% rename from packages/tests/src/e2e/m365/DeployOboTab.tests.ts rename to packages/tests/src/e2e/archive/DeployOboTab.tests.bak diff --git a/packages/tests/src/e2e/bot/NotificationBotHappyPathCommon.ts b/packages/tests/src/e2e/archive/NotificationBotHappyPathCommon.bak similarity index 100% rename from packages/tests/src/e2e/bot/NotificationBotHappyPathCommon.ts rename to packages/tests/src/e2e/archive/NotificationBotHappyPathCommon.bak diff --git a/packages/tests/src/e2e/m365/ProvisionApiSpecMessageExtension.tests.ts b/packages/tests/src/e2e/archive/ProvisionApiSpecMessageExtension.tests.bak similarity index 100% rename from packages/tests/src/e2e/m365/ProvisionApiSpecMessageExtension.tests.ts rename to packages/tests/src/e2e/archive/ProvisionApiSpecMessageExtension.tests.bak index 696985c279a..9feba66353f 100644 --- a/packages/tests/src/e2e/m365/ProvisionApiSpecMessageExtension.tests.ts +++ b/packages/tests/src/e2e/archive/ProvisionApiSpecMessageExtension.tests.bak @@ -5,8 +5,8 @@ * @author Yuqi Zhou */ -import { describe } from "mocha"; import * as chai from "chai"; +import { describe } from "mocha"; import * as path from "path"; import { it } from "@microsoft/extra-shot-mocha"; diff --git a/packages/tests/src/e2e/vs/ProvisionAppServiceNotificationBot.dotnet.tests.ts b/packages/tests/src/e2e/archive/ProvisionAppServiceNotificationBot.dotnet.tests.bak similarity index 100% rename from packages/tests/src/e2e/vs/ProvisionAppServiceNotificationBot.dotnet.tests.ts rename to packages/tests/src/e2e/archive/ProvisionAppServiceNotificationBot.dotnet.tests.bak diff --git a/packages/tests/src/e2e/feature/upgrade/incoming-webhook-notification.tests.ts b/packages/tests/src/e2e/archive/incoming-webhook-notification.tests.bak similarity index 100% rename from packages/tests/src/e2e/feature/upgrade/incoming-webhook-notification.tests.ts rename to packages/tests/src/e2e/archive/incoming-webhook-notification.tests.bak diff --git a/packages/tests/src/e2e/multienv/TestRemoteHappyPathSPFx.tests.ts b/packages/tests/src/e2e/archive/spfx/TestRemoteHappyPathSPFx.tests.bak similarity index 100% rename from packages/tests/src/e2e/multienv/TestRemoteHappyPathSPFx.tests.ts rename to packages/tests/src/e2e/archive/spfx/TestRemoteHappyPathSPFx.tests.bak diff --git a/packages/tests/src/e2e/debug/DebugCustomCopilotAgentAssistantsApiBotForPython.tests.ts b/packages/tests/src/e2e/archive/teamsAgent/DebugCustomCopilotAgentAssistantsApiBotForPython.tests.bak similarity index 97% rename from packages/tests/src/e2e/debug/DebugCustomCopilotAgentAssistantsApiBotForPython.tests.ts rename to packages/tests/src/e2e/archive/teamsAgent/DebugCustomCopilotAgentAssistantsApiBotForPython.tests.bak index 8938d03e94f..b55d4e5b848 100644 --- a/packages/tests/src/e2e/debug/DebugCustomCopilotAgentAssistantsApiBotForPython.tests.ts +++ b/packages/tests/src/e2e/archive/teamsAgent/DebugCustomCopilotAgentAssistantsApiBotForPython.tests.bak @@ -12,13 +12,14 @@ import * as path from "path"; import { it } from "@microsoft/extra-shot-mocha"; -import { CliHelper } from "../../commonlib/cliHelper"; +import { CliHelper } from "../../../commonlib/cliHelper"; +import { execAsync } from "../../../utils/commonUtils"; import { cleanUpLocalProject, getTestFolder, getUniqueAppName, readContextMultiEnvV3, -} from "../commonUtils"; +} from "../../commonUtils"; import { deleteAadAppByClientId, deleteBot, @@ -26,8 +27,7 @@ import { getAadAppByClientId, getBot, getTeamsApp, -} from "./utility"; -import { execAsync } from "../../utils/commonUtils"; +} from "../../debug/utility"; describe("Debug V3 command-and-response template", () => { const testFolder = getTestFolder(); diff --git a/packages/tests/src/e2e/debug/DebugCustomCopilotAgentNewBot.tests.ts b/packages/tests/src/e2e/archive/teamsAgent/DebugCustomCopilotAgentNewBot.tests.bak similarity index 98% rename from packages/tests/src/e2e/debug/DebugCustomCopilotAgentNewBot.tests.ts rename to packages/tests/src/e2e/archive/teamsAgent/DebugCustomCopilotAgentNewBot.tests.bak index 3b630e7a1b3..02b0264337d 100644 --- a/packages/tests/src/e2e/debug/DebugCustomCopilotAgentNewBot.tests.ts +++ b/packages/tests/src/e2e/archive/teamsAgent/DebugCustomCopilotAgentNewBot.tests.bak @@ -6,19 +6,18 @@ */ import * as chai from "chai"; -import * as fs from "fs-extra"; import { describe } from "mocha"; import * as path from "path"; import { it } from "@microsoft/extra-shot-mocha"; -import { CliHelper } from "../../commonlib/cliHelper"; +import { CliHelper } from "../../../commonlib/cliHelper"; import { cleanUpLocalProject, getTestFolder, getUniqueAppName, readContextMultiEnvV3, -} from "../commonUtils"; +} from "../../commonUtils"; import { deleteAadAppByClientId, deleteBot, @@ -26,8 +25,7 @@ import { getAadAppByClientId, getBot, getTeamsApp, -} from "./utility"; -import { execAsync } from "../../utils/commonUtils"; +} from "../../debug/utility"; describe("Debug V3 custom-copilot-agent-new TypeScript template", () => { const testFolder = getTestFolder(); diff --git a/packages/tests/src/e2e/debug/DebugCustomCopilotAgentNewBotForPython.tests.ts b/packages/tests/src/e2e/archive/teamsAgent/DebugCustomCopilotAgentNewBotForPython.tests.bak similarity index 97% rename from packages/tests/src/e2e/debug/DebugCustomCopilotAgentNewBotForPython.tests.ts rename to packages/tests/src/e2e/archive/teamsAgent/DebugCustomCopilotAgentNewBotForPython.tests.bak index b74fe036f62..55d6847767d 100644 --- a/packages/tests/src/e2e/debug/DebugCustomCopilotAgentNewBotForPython.tests.ts +++ b/packages/tests/src/e2e/archive/teamsAgent/DebugCustomCopilotAgentNewBotForPython.tests.bak @@ -12,13 +12,14 @@ import * as path from "path"; import { it } from "@microsoft/extra-shot-mocha"; -import { CliHelper } from "../../commonlib/cliHelper"; +import { CliHelper } from "../../../commonlib/cliHelper"; +import { execAsync } from "../../../utils/commonUtils"; import { cleanUpLocalProject, getTestFolder, getUniqueAppName, readContextMultiEnvV3, -} from "../commonUtils"; +} from "../../commonUtils"; import { deleteAadAppByClientId, deleteBot, @@ -26,8 +27,7 @@ import { getAadAppByClientId, getBot, getTeamsApp, -} from "./utility"; -import { execAsync } from "../../utils/commonUtils"; +} from "../../debug/utility"; describe("Debug V3 command-and-response template", () => { const testFolder = getTestFolder(); diff --git a/packages/tests/src/e2e/debug/DebugCustomCopilotBasicBotForPython.tests.ts b/packages/tests/src/e2e/archive/teamsAgent/DebugCustomCopilotBasicBotForPython.tests.bak similarity index 97% rename from packages/tests/src/e2e/debug/DebugCustomCopilotBasicBotForPython.tests.ts rename to packages/tests/src/e2e/archive/teamsAgent/DebugCustomCopilotBasicBotForPython.tests.bak index a6bf3eb7fd2..8f03fb93a6a 100644 --- a/packages/tests/src/e2e/debug/DebugCustomCopilotBasicBotForPython.tests.ts +++ b/packages/tests/src/e2e/archive/teamsAgent/DebugCustomCopilotBasicBotForPython.tests.bak @@ -12,13 +12,14 @@ import * as path from "path"; import { it } from "@microsoft/extra-shot-mocha"; -import { CliHelper } from "../../commonlib/cliHelper"; +import { CliHelper } from "../../../commonlib/cliHelper"; +import { execAsync } from "../../../utils/commonUtils"; import { cleanUpLocalProject, getTestFolder, getUniqueAppName, readContextMultiEnvV3, -} from "../commonUtils"; +} from "../../commonUtils"; import { deleteAadAppByClientId, deleteBot, @@ -26,8 +27,7 @@ import { getAadAppByClientId, getBot, getTeamsApp, -} from "./utility"; -import { execAsync } from "../../utils/commonUtils"; +} from "../../debug/utility"; describe("Debug V3 command-and-response template", () => { const testFolder = getTestFolder(); diff --git a/packages/tests/src/e2e/debug/DebugCustomCopilotRagAiSearchBotForPython.tests.ts b/packages/tests/src/e2e/archive/teamsAgent/DebugCustomCopilotRagAiSearchBotForPython.tests.bak similarity index 97% rename from packages/tests/src/e2e/debug/DebugCustomCopilotRagAiSearchBotForPython.tests.ts rename to packages/tests/src/e2e/archive/teamsAgent/DebugCustomCopilotRagAiSearchBotForPython.tests.bak index baaba020b0e..1c7384ed035 100644 --- a/packages/tests/src/e2e/debug/DebugCustomCopilotRagAiSearchBotForPython.tests.ts +++ b/packages/tests/src/e2e/archive/teamsAgent/DebugCustomCopilotRagAiSearchBotForPython.tests.bak @@ -12,13 +12,14 @@ import * as path from "path"; import { it } from "@microsoft/extra-shot-mocha"; -import { CliHelper } from "../../commonlib/cliHelper"; +import { CliHelper } from "../../../commonlib/cliHelper"; +import { execAsync } from "../../../utils/commonUtils"; import { cleanUpLocalProject, getTestFolder, getUniqueAppName, readContextMultiEnvV3, -} from "../commonUtils"; +} from "../../commonUtils"; import { deleteAadAppByClientId, deleteBot, @@ -26,8 +27,7 @@ import { getAadAppByClientId, getBot, getTeamsApp, -} from "./utility"; -import { execAsync } from "../../utils/commonUtils"; +} from "../../debug/utility"; describe("Debug V3 command-and-response template", () => { const testFolder = getTestFolder(); diff --git a/packages/tests/src/e2e/debug/DebugCustomCopilotRagBasicBotForPython.tests.ts b/packages/tests/src/e2e/archive/teamsAgent/DebugCustomCopilotRagBasicBotForPython.tests.bak similarity index 97% rename from packages/tests/src/e2e/debug/DebugCustomCopilotRagBasicBotForPython.tests.ts rename to packages/tests/src/e2e/archive/teamsAgent/DebugCustomCopilotRagBasicBotForPython.tests.bak index 454bd5bb520..840a562faff 100644 --- a/packages/tests/src/e2e/debug/DebugCustomCopilotRagBasicBotForPython.tests.ts +++ b/packages/tests/src/e2e/archive/teamsAgent/DebugCustomCopilotRagBasicBotForPython.tests.bak @@ -12,13 +12,14 @@ import * as path from "path"; import { it } from "@microsoft/extra-shot-mocha"; -import { CliHelper } from "../../commonlib/cliHelper"; +import { CliHelper } from "../../../commonlib/cliHelper"; +import { execAsync } from "../../../utils/commonUtils"; import { cleanUpLocalProject, getTestFolder, getUniqueAppName, readContextMultiEnvV3, -} from "../commonUtils"; +} from "../../commonUtils"; import { deleteAadAppByClientId, deleteBot, @@ -26,8 +27,7 @@ import { getAadAppByClientId, getBot, getTeamsApp, -} from "./utility"; -import { execAsync } from "../../utils/commonUtils"; +} from "../../debug/utility"; describe("Debug V3 command-and-response template", () => { const testFolder = getTestFolder(); diff --git a/packages/tests/src/e2e/bot/CommandBotHappyPathCommon.ts b/packages/tests/src/e2e/bot/CommandBotHappyPathCommon.ts deleted file mode 100644 index 90df255cf82..00000000000 --- a/packages/tests/src/e2e/bot/CommandBotHappyPathCommon.ts +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -/** - * @author Siglud - */ - -import * as path from "path"; - -import { it } from "@microsoft/extra-shot-mocha"; -import { environmentNameManager } from "@microsoft/teamsfx-core/build/core/environmentName"; -import { AppStudioValidator, BotValidator } from "../../commonlib"; -import { Runtime } from "../../commonlib/constants"; -import { - cleanUp, - execAsync, - execAsyncWithRetry, - getTestFolder, - getUniqueAppName, - readContextMultiEnvV3, - createResourceGroup, -} from "../commonUtils"; -import { Executor } from "../../utils/executor"; -import { expect } from "chai"; - -export function happyPathTest(runtime: Runtime): void { - describe("Provision", function () { - const testFolder = getTestFolder(); - const appName = getUniqueAppName(); - const projectPath = path.resolve(testFolder, appName); - const envName = environmentNameManager.getDefaultEnvName(); - let teamsAppId: string | undefined; - - const env = Object.assign({}, process.env); - if (runtime === Runtime.Dotnet) { - env["TEAMSFX_CLI_DOTNET"] = "true"; - if (process.env["DOTNET_ROOT"]) { - env[ - "PATH" - ] = `${process.env["DOTNET_ROOT"]}${path.delimiter}${process.env["PATH"]}`; - } - } - - it("Provision Resource: command and response", async function () { - const cmd = - runtime === Runtime.Node - ? `atk new --interactive false --app-name ${appName} --capability command-bot --programming-language typescript` - : `atk new --interactive false --runtime ${runtime} --app-name ${appName} --capability command-bot`; - await execAsync(cmd, { - cwd: testFolder, - env: env, - timeout: 0, - }); - console.log(`[Successfully] scaffold to ${projectPath}`); - - // provision - const result = await createResourceGroup(appName + "-rg", "westus"); - expect(result).to.be.true; - process.env["AZURE_RESOURCE_GROUP_NAME"] = appName + "-rg"; - const { success } = await Executor.provision(projectPath, envName); - expect(success).to.be.true; - console.log(`[Successfully] provision for ${projectPath}`); - - { - // Validate provision - // Get context - const context = await readContextMultiEnvV3(projectPath, envName); - teamsAppId = context.TEAMS_APP_ID; - AppStudioValidator.setE2ETestProvider(); - - // Validate Bot Provision - const bot = new BotValidator(context, projectPath, envName); - await bot.validateProvisionV3(false); - } - - // deploy - const cmdStr = "atk deploy"; - await execAsyncWithRetry(cmdStr, { - cwd: projectPath, - env: env, - timeout: 0, - }); - console.log(`[Successfully] deploy for ${projectPath}`); - - { - // Validate deployment - - // Get context - const context = await readContextMultiEnvV3(projectPath, envName); - - // Validate Bot Deploy - const bot = new BotValidator(context, projectPath, envName); - await bot.validateDeploy(); - } - - // test (validate) - await execAsyncWithRetry(`atk validate --env ${envName}`, { - cwd: projectPath, - env: env, - timeout: 0, - }); - - // package - await execAsyncWithRetry(`atk package --env ${envName}`, { - cwd: projectPath, - env: env, - timeout: 0, - }); - - // publish only run on node - if (runtime !== Runtime.Dotnet) { - await execAsyncWithRetry(`atk publish --env ${envName}`, { - cwd: projectPath, - env: process.env, - timeout: 0, - }); - - { - // Validate publish result - await AppStudioValidator.validatePublish(teamsAppId!); - } - } - }); - - this.afterEach(async () => { - console.log(`[Successfully] start to clean up for ${projectPath}`); - await cleanUp( - appName, - projectPath, - false, - true, - false, - envName, - teamsAppId - ); - }); - }); -} diff --git a/packages/tests/src/e2e/bot/EchoBotHappyPath.tests.ts b/packages/tests/src/e2e/bot/EchoBotHappyPath.tests.ts deleted file mode 100644 index 201b6273af3..00000000000 --- a/packages/tests/src/e2e/bot/EchoBotHappyPath.tests.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -/** - * @author yukun-dong - */ - -import { happyPathTest } from "./BotHappyPathCommon"; -import { Runtime } from "../../commonlib/constants"; -import { it } from "@microsoft/extra-shot-mocha"; - -describe("Remote happy path for echo bot node", () => { - it( - "Remote happy path for echo bot node", - { testPlanCaseId: 24907383, author: "yukundong@microsoft.com" }, - async function () { - await happyPathTest(Runtime.Node, "bot"); - } - ); -}); diff --git a/packages/tests/src/e2e/bot/MessageExtensionActionHappyPath.tests.ts b/packages/tests/src/e2e/bot/MessageExtensionActionHappyPath.tests.ts deleted file mode 100644 index bd04f60d4f9..00000000000 --- a/packages/tests/src/e2e/bot/MessageExtensionActionHappyPath.tests.ts +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -/** - * @author Siglud - **/ -import { it } from "@microsoft/extra-shot-mocha"; -import { Runtime } from "../../commonlib/constants"; -import { happyPathTest } from "./BotHappyPathCommon"; - -describe("Provision message extension Node template", () => { - it( - "Provision Template: message extension for NodeJS", - { testPlanCaseId: 15685647, author: "fanhu@microsoft.com" }, - async function () { - await happyPathTest(Runtime.Node, "basic-message-extension"); - } - ); -}); diff --git a/packages/tests/src/e2e/bot/WorkflowBotHappyPathCommon.ts b/packages/tests/src/e2e/bot/WorkflowBotHappyPathCommon.ts deleted file mode 100644 index f8c8c6c4448..00000000000 --- a/packages/tests/src/e2e/bot/WorkflowBotHappyPathCommon.ts +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -/** - * @author Siglud - */ - -import * as fs from "fs-extra"; -import * as path from "path"; - -import { BotValidator } from "../../commonlib"; - -import { - execAsync, - execAsyncWithRetry, - getSubscriptionId, - getTestFolder, - getUniqueAppName, - cleanUp, - readContextMultiEnv, - readContextMultiEnvV3, - createResourceGroup, -} from "../commonUtils"; -import { environmentNameManager } from "@microsoft/teamsfx-core"; -import { it } from "../../commonlib/it"; -import { Runtime } from "../../commonlib/constants"; -import { Executor } from "../../utils/executor"; -import { expect } from "chai"; - -export function happyPathTest(runtime: Runtime): void { - describe("Provision", function () { - const testFolder = getTestFolder(); - const appName = getUniqueAppName(); - const subscription = getSubscriptionId(); - const projectPath = path.resolve(testFolder, appName); - const envName = environmentNameManager.getDefaultEnvName(); - - const env = Object.assign({}, process.env); - env["TEAMSFX_CONFIG_UNIFY"] = "true"; - env["BOT_NOTIFICATION_ENABLED"] = "true"; - env["TEAMSFX_TEMPLATE_PRERELEASE"] = "alpha"; - if (runtime === Runtime.Dotnet) { - env["TEAMSFX_CLI_DOTNET"] = "true"; - } - - it("Provision Resource: workflow bot", async function () { - const cmd = - runtime === Runtime.Node - ? `atk new --interactive false --app-name ${appName} --capability workflow-bot --programming-language typescript` - : `atk new --interactive false --runtime ${runtime} --app-name ${appName} --capability workflow-bot`; - await execAsync(cmd, { - cwd: testFolder, - env: env, - timeout: 0, - }); - console.log(`[Successfully] scaffold to ${projectPath}`); - - // provision - const result = await createResourceGroup(appName + "-rg", "westus"); - expect(result).to.be.true; - process.env["AZURE_RESOURCE_GROUP_NAME"] = appName + "-rg"; - const { success } = await Executor.provision(projectPath, envName); - expect(success).to.be.true; - console.log(`[Successfully] provision for ${projectPath}`); - - { - // Validate provision - // Get context - const context = await readContextMultiEnvV3(projectPath, envName); - - // Validate Bot Provision - const bot = new BotValidator(context, projectPath, envName); - await bot.validateProvisionV3(false); - } - - // deploy - await execAsyncWithRetry(`atk deploy`, { - cwd: projectPath, - env: env, - timeout: 0, - }); - console.log(`[Successfully] deploy for ${projectPath}`); - - { - // Validate deployment - - // Get context - const context = await readContextMultiEnvV3(projectPath, envName); - - // Validate Bot Deploy - const bot = new BotValidator(context, projectPath, envName); - await bot.validateDeploy(); - } - - // test (validate) - await execAsyncWithRetry(`atk validate --env ${envName}`, { - cwd: projectPath, - env: env, - timeout: 0, - }); - - // package - await execAsyncWithRetry(`atk package --env ${envName}`, { - cwd: projectPath, - env: env, - timeout: 0, - }); - }); - - this.afterEach(async () => { - console.log(`[Successfully] start to clean up for ${projectPath}`); - await cleanUp(appName, projectPath, false, true, false); - }); - }); -} diff --git a/packages/tests/src/e2e/debug/DebugCommandAndResponse.tests.ts b/packages/tests/src/e2e/debug/DebugCommandAndResponse.tests.ts deleted file mode 100644 index 78b325b2a4b..00000000000 --- a/packages/tests/src/e2e/debug/DebugCommandAndResponse.tests.ts +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -/** - * @author Kuojian Lu - */ - -import * as chai from "chai"; -import * as fs from "fs-extra"; -import { describe } from "mocha"; -import * as path from "path"; - -import { it } from "@microsoft/extra-shot-mocha"; - -import { CliHelper } from "../../commonlib/cliHelper"; -import { - cleanUpLocalProject, - getTestFolder, - getUniqueAppName, - readContextMultiEnvV3, -} from "../commonUtils"; -import { - deleteAadAppByClientId, - deleteBot, - deleteTeamsApp, - getAadAppByClientId, - getBot, - getTeamsApp, -} from "./utility"; - -describe("Debug V3 command-and-response template", () => { - const testFolder = getTestFolder(); - const appName = getUniqueAppName(); - const projectPath = path.resolve(testFolder, appName); - - afterEach(async function () { - const context = await readContextMultiEnvV3(projectPath, "local"); - - // clean up - if (context?.TEAMS_APP_ID) { - await deleteTeamsApp(context.TEAMS_APP_ID); - } - if (context?.BOT_ID) { - await deleteBot(context.BOT_ID); - await deleteAadAppByClientId(context.BOT_ID); - } - await cleanUpLocalProject(projectPath); - }); - - it( - "happy path: provision and deploy", - { testPlanCaseId: 15685858, author: "kuojianlu@microsoft.com" }, - async function () { - // create - await CliHelper.createProjectWithCapability( - appName, - testFolder, - "command-bot" as any - ); - console.log(`[Successfully] scaffold to ${projectPath}`); - - // provision - await CliHelper.provisionProject(projectPath, "", "local", { - ...process.env, - BOT_DOMAIN: "test.ngrok.io", - BOT_ENDPOINT: "https://test.ngrok.io", - }); - console.log(`[Successfully] provision for ${projectPath}`); - - let context = await readContextMultiEnvV3(projectPath, "local"); - chai.assert.isDefined(context); - - // validate bot - chai.assert.isDefined(context.BOT_ID); - chai.assert.isNotEmpty(context.BOT_ID); - const aadApp = await getAadAppByClientId(context.BOT_ID); - chai.assert.isDefined(aadApp); - chai.assert.equal(aadApp?.appId, context.BOT_ID); - const bot = await getBot(context.BOT_ID); - chai.assert.equal(bot?.botId, context.BOT_ID); - chai.assert.equal( - bot?.messagingEndpoint, - "https://test.ngrok.io/api/messages" - ); - chai.assert.deepEqual(bot?.configuredChannels, ["msteams"]); - - // validate teams app - chai.assert.isDefined(context.TEAMS_APP_ID); - const teamsApp = await getTeamsApp(context.TEAMS_APP_ID); - chai.assert.equal(teamsApp?.teamsAppId, context.TEAMS_APP_ID); - - // deploy - await CliHelper.deployAll(projectPath, "", "local"); - console.log(`[Successfully] deploy for ${projectPath}`); - - context = await readContextMultiEnvV3(projectPath, "local"); - chai.assert.isDefined(context); - - // validate .localConfigs - chai.assert.isTrue( - await fs.pathExists(path.join(projectPath, ".localConfigs")) - ); - } - ); -}); diff --git a/packages/tests/src/e2e/debug/DebugNonSsoTab.tests.ts b/packages/tests/src/e2e/debug/DebugNonSsoTab.tests.ts deleted file mode 100644 index d98f10f7fed..00000000000 --- a/packages/tests/src/e2e/debug/DebugNonSsoTab.tests.ts +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -/** - * @author Kuojian Lu - */ - -import * as chai from "chai"; -import * as fs from "fs-extra"; -import { describe } from "mocha"; -import * as path from "path"; - -import { it } from "@microsoft/extra-shot-mocha"; - -import { CliHelper } from "../../commonlib/cliHelper"; -import { Capability } from "../../utils/constants"; -import { - cleanUpLocalProject, - getTestFolder, - getUniqueAppName, - readContextMultiEnvV3, -} from "../commonUtils"; -import { deleteTeamsApp, getTeamsApp } from "./utility"; -import { removeTeamsAppExtendToM365 } from "../commonUtils"; - -describe("Debug V3 tab-non-sso template", () => { - const testFolder = getTestFolder(); - const appName = getUniqueAppName(); - const projectPath = path.resolve(testFolder, appName); - - afterEach(async function () { - // clean up - const context = await readContextMultiEnvV3(projectPath, "local"); - if (context?.TEAMS_APP_ID) { - await deleteTeamsApp(context.TEAMS_APP_ID); - } - await cleanUpLocalProject(projectPath); - }); - - it( - "happy path: provision and deploy", - { testPlanCaseId: 9426074, author: "kuojianlu@microsoft.com" }, - async function () { - // create - await CliHelper.createProjectWithCapability( - appName, - testFolder, - Capability.TabNonSso - ); - console.log(`[Successfully] scaffold to ${projectPath}`); - - // remove teamsApp/extendToM365 in case it fails - removeTeamsAppExtendToM365(path.join(projectPath, "teamsapp.local.yml")); - - // provision - await CliHelper.provisionProject(projectPath, "", "local"); - console.log(`[Successfully] provision for ${projectPath}`); - - let context = await readContextMultiEnvV3(projectPath, "local"); - chai.assert.isDefined(context); - - // validate aad - chai.assert.isUndefined(context.AAD_APP_OBJECT_ID); - - // validate teams app - chai.assert.isDefined(context.TEAMS_APP_ID); - const teamsApp = await getTeamsApp(context.TEAMS_APP_ID); - chai.assert.equal(teamsApp?.teamsAppId, context.TEAMS_APP_ID); - - // deploy - await CliHelper.deployAll(projectPath, "", "local"); - console.log(`[Successfully] deploy for ${projectPath}`); - - context = await readContextMultiEnvV3(projectPath, "local"); - chai.assert.isDefined(context); - - // validate ssl cert - chai.assert.isDefined(context.SSL_CRT_FILE); - chai.assert.isNotEmpty(context.SSL_CRT_FILE); - chai.assert.isDefined(context.SSL_KEY_FILE); - chai.assert.isNotEmpty(context.SSL_KEY_FILE); - - // validate .localConfigs - chai.assert.isTrue( - await fs.pathExists(path.join(projectPath, ".localConfigs")) - ); - } - ); -}); diff --git a/packages/tests/src/e2e/debug/DebugNotificationHttpTrigger.tests.ts b/packages/tests/src/e2e/debug/DebugNotificationHttpTrigger.tests.ts deleted file mode 100644 index 6b2e08fdfe0..00000000000 --- a/packages/tests/src/e2e/debug/DebugNotificationHttpTrigger.tests.ts +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -/** - * @author Kuojian Lu - */ - -import * as chai from "chai"; -import * as fs from "fs-extra"; -import { describe } from "mocha"; -import * as path from "path"; - -import { it } from "@microsoft/extra-shot-mocha"; - -import { CliHelper } from "../../commonlib/cliHelper"; -import { Capability } from "../../utils/constants"; -import { - cleanUpLocalProject, - getTestFolder, - getUniqueAppName, - readContextMultiEnvV3, -} from "../commonUtils"; -import { - deleteAadAppByClientId, - deleteBot, - deleteTeamsApp, - getAadAppByClientId, - getBot, - getTeamsApp, -} from "./utility"; - -describe("Debug V3 notification-http-trigger template", () => { - const testFolder = getTestFolder(); - const appName = getUniqueAppName(); - const projectPath = path.resolve(testFolder, appName); - - afterEach(async function () { - const context = await readContextMultiEnvV3(projectPath, "local"); - - // clean up - if (context?.TEAMS_APP_ID) { - await deleteTeamsApp(context.TEAMS_APP_ID); - } - if (context?.BOT_ID) { - await deleteBot(context.BOT_ID); - await deleteAadAppByClientId(context.BOT_ID); - } - await cleanUpLocalProject(projectPath); - }); - - it( - "happy path: provision and deploy", - { testPlanCaseId: 24132570, author: "kuojianlu@microsoft.com" }, - async function () { - // create - await CliHelper.createProjectWithCapability( - appName, - testFolder, - Capability.Notification, - undefined, - "--bot-host-type-trigger http-functions" - ); - console.log(`[Successfully] scaffold to ${projectPath}`); - - // provision - await CliHelper.provisionProject(projectPath, "", "local", { - ...process.env, - BOT_DOMAIN: "test.ngrok.io", - BOT_ENDPOINT: "https://test.ngrok.io", - }); - console.log(`[Successfully] provision for ${projectPath}`); - - let context = await readContextMultiEnvV3(projectPath, "local"); - chai.assert.isDefined(context); - - // validate bot - chai.assert.isDefined(context.BOT_ID); - chai.assert.isNotEmpty(context.BOT_ID); - const aadApp = await getAadAppByClientId(context.BOT_ID); - chai.assert.isDefined(aadApp); - chai.assert.equal(aadApp?.appId, context.BOT_ID); - const bot = await getBot(context.BOT_ID); - chai.assert.equal(bot?.botId, context.BOT_ID); - chai.assert.equal( - bot?.messagingEndpoint, - "https://test.ngrok.io/api/messages" - ); - chai.assert.deepEqual(bot?.configuredChannels, ["msteams"]); - - // validate teams app - chai.assert.isDefined(context.TEAMS_APP_ID); - const teamsApp = await getTeamsApp(context.TEAMS_APP_ID); - chai.assert.equal(teamsApp?.teamsAppId, context.TEAMS_APP_ID); - - // deploy - await CliHelper.deployAll(projectPath, "", "local"); - console.log(`[Successfully] deploy for ${projectPath}`); - - context = await readContextMultiEnvV3(projectPath, "local"); - chai.assert.isDefined(context); - - // validate func - chai.assert.isUndefined(context.FUNC_PATH); // FUNC_PATH is undefined for global func - - // validate .localConfigs - chai.assert.isTrue( - await fs.pathExists(path.join(projectPath, ".localConfigs")) - ); - } - ); -}); diff --git a/packages/tests/src/e2e/feature/Permission.tests.ts b/packages/tests/src/e2e/feature/Permission.tests.ts index 06a4f247e3f..83a67d1c4ff 100644 --- a/packages/tests/src/e2e/feature/Permission.tests.ts +++ b/packages/tests/src/e2e/feature/Permission.tests.ts @@ -38,7 +38,7 @@ describe("Collaboration", function () { // new a project await execAsync( - `atk new --interactive false --capability bot --app-name ${appName}`, + `atk new --interactive false --capability declarative-agent --app-name ${appName} --programming-language typescript --with-plugin yes --api-plugin-type new-api --api-auth oauth`, { cwd: testFolder, env: process.env, diff --git a/packages/tests/src/e2e/mosApi/appPackage-da.local.zip b/packages/tests/src/e2e/feature/mosApi/appPackage-da.local.zip similarity index 100% rename from packages/tests/src/e2e/mosApi/appPackage-da.local.zip rename to packages/tests/src/e2e/feature/mosApi/appPackage-da.local.zip diff --git a/packages/tests/src/e2e/mosApi/appPackage.local.zip b/packages/tests/src/e2e/feature/mosApi/appPackage.local.zip similarity index 100% rename from packages/tests/src/e2e/mosApi/appPackage.local.zip rename to packages/tests/src/e2e/feature/mosApi/appPackage.local.zip diff --git a/packages/tests/src/e2e/mosApi/sideloading.tests.ts b/packages/tests/src/e2e/feature/mosApi/sideloading.tests.ts similarity index 96% rename from packages/tests/src/e2e/mosApi/sideloading.tests.ts rename to packages/tests/src/e2e/feature/mosApi/sideloading.tests.ts index 270320da3dd..eb564d1641d 100644 --- a/packages/tests/src/e2e/mosApi/sideloading.tests.ts +++ b/packages/tests/src/e2e/feature/mosApi/sideloading.tests.ts @@ -6,9 +6,9 @@ */ import { it } from "@microsoft/extra-shot-mocha"; -import path from "path"; -import { M365TitleHelper } from "../../commonlib/m365TitleHelper"; import { assert } from "chai"; +import path from "path"; +import { M365TitleHelper } from "../../../commonlib/m365TitleHelper"; describe("MOS3 API", function () { it( diff --git a/packages/tests/src/e2e/multienv/TestRemoteHappyPath.tests.ts b/packages/tests/src/e2e/feature/multienv.tests.ts similarity index 98% rename from packages/tests/src/e2e/multienv/TestRemoteHappyPath.tests.ts rename to packages/tests/src/e2e/feature/multienv.tests.ts index e130ecaa723..afb1eed124b 100644 --- a/packages/tests/src/e2e/multienv/TestRemoteHappyPath.tests.ts +++ b/packages/tests/src/e2e/feature/multienv.tests.ts @@ -5,28 +5,28 @@ * @author Yuan Tian */ +import { it } from "@microsoft/extra-shot-mocha"; +import M365Login from "@microsoft/m365agentstoolkit-cli/src/commonlib/m365Login"; import { AppPackageFolderName, BuildFolderName } from "@microsoft/teamsfx-api"; import * as chai from "chai"; +import { expect } from "chai"; import fs from "fs-extra"; import { describe } from "mocha"; import path from "path"; -import M365Login from "@microsoft/m365agentstoolkit-cli/src/commonlib/m365Login"; import { AppStudioValidator, BotValidator } from "../../commonlib"; import { CliHelper } from "../../commonlib/cliHelper"; +import { EnvConstants } from "../../commonlib/constants"; +import { Executor } from "../../utils/executor"; import { cleanUp, + createResourceGroup, execAsync, execAsyncWithRetry, getTestFolder, getUniqueAppName, mockTeamsfxMultiEnvFeatureFlag, readContextMultiEnvV3, - createResourceGroup, } from "../commonUtils"; -import { expect } from "chai"; -import { Executor } from "../../utils/executor"; -import { it } from "@microsoft/extra-shot-mocha"; -import { EnvConstants } from "../../commonlib/constants"; describe("Multi Env Happy Path for Azure", function () { const env = "e2e"; @@ -43,7 +43,7 @@ describe("Multi Env Happy Path for Azure", function () { try { let result; result = await execAsync( - `atk new --interactive false --app-name ${appName} --capability notification --bot-host-type-trigger http-functions --programming-language javascript`, + `atk new --interactive false --app-name ${appName} --capability bot --programming-language typescript`, { cwd: testFolder, env: processEnv, diff --git a/packages/tests/src/e2e/m365/DebugM365MessageExtension.tests.ts b/packages/tests/src/e2e/m365/DebugM365MessageExtension.tests.ts deleted file mode 100644 index 3ced98e7567..00000000000 --- a/packages/tests/src/e2e/m365/DebugM365MessageExtension.tests.ts +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -/** - * @author Kuojian Lu - */ - -import * as chai from "chai"; -import * as fs from "fs-extra"; -import { describe } from "mocha"; -import * as path from "path"; - -import { it } from "@microsoft/extra-shot-mocha"; - -import { CliHelper } from "../../commonlib/cliHelper"; -import { Capability } from "../../utils/constants"; -import { - cleanUpLocalProject, - getTestFolder, - getUniqueAppName, - readContextMultiEnvV3, -} from "../commonUtils"; -import { - deleteAadAppByClientId, - deleteBot, - deleteTeamsApp, - getAadAppByClientId, - getBot, - getTeamsApp, -} from "../debug/utility"; - -describe("Debug V3 m365-message-extension template", () => { - const testFolder = getTestFolder(); - const appName = getUniqueAppName(); - const projectPath = path.resolve(testFolder, appName); - - afterEach(async function () { - const context = await readContextMultiEnvV3(projectPath, "local"); - - // clean up - if (context?.TEAMS_APP_ID) { - await deleteTeamsApp(context.TEAMS_APP_ID); - } - if (context?.BOT_ID) { - await deleteBot(context.BOT_ID); - await deleteAadAppByClientId(context.BOT_ID); - } - await cleanUpLocalProject(projectPath); - }); - - it( - "happy path: provision and deploy", - { testPlanCaseId: 17449538, author: "kuojianlu@microsoft.com" }, - async function () { - // create - await CliHelper.createProjectWithCapability( - appName, - testFolder, - Capability.MessageExtension, - undefined, - "--me-architecture bot" - ); - console.log(`[Successfully] scaffold to ${projectPath}`); - - // provision - await CliHelper.provisionProject(projectPath, "", "local", { - ...process.env, - BOT_DOMAIN: "test.ngrok.io", - BOT_ENDPOINT: "https://test.ngrok.io", - }); - console.log(`[Successfully] provision for ${projectPath}`); - - let context = await readContextMultiEnvV3(projectPath, "local"); - chai.assert.isDefined(context); - - // validate bot - chai.assert.isDefined(context.BOT_ID); - chai.assert.isNotEmpty(context.BOT_ID); - const aadApp = await getAadAppByClientId(context.BOT_ID); - chai.assert.isDefined(aadApp); - chai.assert.equal(aadApp?.appId, context.BOT_ID); - const bot = await getBot(context.BOT_ID); - chai.assert.equal(bot?.botId, context.BOT_ID); - chai.assert.equal( - bot?.messagingEndpoint, - "https://test.ngrok.io/api/messages" - ); - chai.assert.deepEqual(bot?.configuredChannels, [ - "msteams", - "m365extensions", - ]); - - // validate teams app - chai.assert.isDefined(context.TEAMS_APP_ID); - const teamsApp = await getTeamsApp(context.TEAMS_APP_ID); - chai.assert.equal(teamsApp?.teamsAppId, context.TEAMS_APP_ID); - - // validate m365 - chai.assert.isDefined(context.M365_TITLE_ID); - chai.assert.isNotEmpty(context.M365_TITLE_ID); - chai.assert.isDefined(context.M365_APP_ID); - chai.assert.isNotEmpty(context.M365_APP_ID); - - // deploy - await CliHelper.deployAll(projectPath, "", "local"); - console.log(`[Successfully] deploy for ${projectPath}`); - - context = await readContextMultiEnvV3(projectPath, "local"); - chai.assert.isDefined(context); - - // validate .localConfigs - chai.assert.isTrue( - await fs.pathExists(path.join(projectPath, ".localConfigs")) - ); - } - ); -}); diff --git a/packages/tests/src/e2e/m365/DeployM365MessageExtension.tests.ts b/packages/tests/src/e2e/m365/DeployM365MessageExtension.tests.ts deleted file mode 100644 index 00fe3b6c4e6..00000000000 --- a/packages/tests/src/e2e/m365/DeployM365MessageExtension.tests.ts +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -/** - * @author Kuojian Lu - */ - -import * as chai from "chai"; -import { describe } from "mocha"; -import * as path from "path"; - -import { it } from "@microsoft/extra-shot-mocha"; - -import { CliHelper } from "../../commonlib/cliHelper"; -import { Capability } from "../../utils/constants"; -import { - cleanUpLocalProject, - createResourceGroup, - deleteResourceGroupByName, - getTestFolder, - getUniqueAppName, - readContextMultiEnvV3, -} from "../commonUtils"; -import { deleteTeamsApp, getTeamsApp } from "../debug/utility"; - -describe("Deploy V3 m365-message-extension template", () => { - const testFolder = getTestFolder(); - const appName = getUniqueAppName(); - const projectPath = path.resolve(testFolder, appName); - const resourceGroupName = `${appName}-rg`; - - afterEach(async function () { - // clean up - const context = await readContextMultiEnvV3(projectPath, "dev"); - if (context?.TEAMS_APP_ID) { - await deleteTeamsApp(context.TEAMS_APP_ID); - } - await deleteResourceGroupByName(resourceGroupName); - await cleanUpLocalProject(projectPath); - }); - - it( - "happy path: provision and deploy", - { testPlanCaseId: 17449554, author: "kuojianlu@microsoft.com" }, - async function () { - // create - await CliHelper.createProjectWithCapability( - appName, - testFolder, - Capability.MessageExtension, - undefined, - "--me-architecture bot" - ); - console.log(`[Successfully] scaffold to ${projectPath}`); - - // provision - const result = await createResourceGroup(resourceGroupName, "westus"); - chai.assert.isTrue(result); - - await CliHelper.provisionProject(projectPath, "", "dev", { - ...process.env, - AZURE_RESOURCE_GROUP_NAME: resourceGroupName, - }); - console.log(`[Successfully] provision for ${projectPath}`); - - let context = await readContextMultiEnvV3(projectPath, "dev"); - chai.assert.isDefined(context); - - // validate teams app - chai.assert.isDefined(context.TEAMS_APP_ID); - const teamsApp = await getTeamsApp(context.TEAMS_APP_ID); - chai.assert.equal(teamsApp?.teamsAppId, context.TEAMS_APP_ID); - - // validate bot id - chai.assert.isDefined(context.BOT_ID); - chai.assert.isNotEmpty(context.BOT_ID); - - // validate m365 - chai.assert.isDefined(context.M365_TITLE_ID); - chai.assert.isNotEmpty(context.M365_TITLE_ID); - chai.assert.isDefined(context.M365_APP_ID); - chai.assert.isNotEmpty(context.M365_APP_ID); - - // deploy - await CliHelper.deployAll(projectPath, "", "dev"); - console.log(`[Successfully] deploy for ${projectPath}`); - - context = await readContextMultiEnvV3(projectPath, "dev"); - chai.assert.isDefined(context); - } - ); -}); diff --git a/packages/tests/src/e2e/debug/DebugCustomCopilotBasicBot.tests.ts b/packages/tests/src/e2e/teamsAgent/DebugCustomCopilotBasicBot.tests.ts similarity index 98% rename from packages/tests/src/e2e/debug/DebugCustomCopilotBasicBot.tests.ts rename to packages/tests/src/e2e/teamsAgent/DebugCustomCopilotBasicBot.tests.ts index 144745d3379..35be02d40fa 100644 --- a/packages/tests/src/e2e/debug/DebugCustomCopilotBasicBot.tests.ts +++ b/packages/tests/src/e2e/teamsAgent/DebugCustomCopilotBasicBot.tests.ts @@ -6,7 +6,6 @@ */ import * as chai from "chai"; -import * as fs from "fs-extra"; import { describe } from "mocha"; import * as path from "path"; @@ -26,8 +25,7 @@ import { getAadAppByClientId, getBot, getTeamsApp, -} from "./utility"; -import { execAsync } from "../../utils/commonUtils"; +} from "../debug/utility"; describe("Debug V3 custom-copilot-basic TypeScript template", () => { const testFolder = getTestFolder(); diff --git a/packages/tests/src/e2e/debug/DebugCustomCopilotRagAiSearchBot.tests.ts b/packages/tests/src/e2e/teamsAgent/DebugCustomCopilotRagAiSearchBot.tests.ts similarity index 99% rename from packages/tests/src/e2e/debug/DebugCustomCopilotRagAiSearchBot.tests.ts rename to packages/tests/src/e2e/teamsAgent/DebugCustomCopilotRagAiSearchBot.tests.ts index 08475f52e88..2e6fefb8814 100644 --- a/packages/tests/src/e2e/debug/DebugCustomCopilotRagAiSearchBot.tests.ts +++ b/packages/tests/src/e2e/teamsAgent/DebugCustomCopilotRagAiSearchBot.tests.ts @@ -26,8 +26,7 @@ import { getAadAppByClientId, getBot, getTeamsApp, -} from "./utility"; -import { execAsync } from "../../utils/commonUtils"; +} from "../debug/utility"; describe("Debug V3 custom-copilot-rag-ai-search TypeScript template", () => { const testFolder = getTestFolder(); diff --git a/packages/tests/src/e2e/debug/DebugCustomCopilotRagBasicBot.tests.ts b/packages/tests/src/e2e/teamsAgent/DebugCustomCopilotRagBasicBot.tests.ts similarity index 99% rename from packages/tests/src/e2e/debug/DebugCustomCopilotRagBasicBot.tests.ts rename to packages/tests/src/e2e/teamsAgent/DebugCustomCopilotRagBasicBot.tests.ts index ef7e63f0068..0ef3af18af8 100644 --- a/packages/tests/src/e2e/debug/DebugCustomCopilotRagBasicBot.tests.ts +++ b/packages/tests/src/e2e/teamsAgent/DebugCustomCopilotRagBasicBot.tests.ts @@ -6,7 +6,6 @@ */ import * as chai from "chai"; -import * as fs from "fs-extra"; import { describe } from "mocha"; import * as path from "path"; @@ -26,8 +25,7 @@ import { getAadAppByClientId, getBot, getTeamsApp, -} from "./utility"; -import { execAsync } from "../../utils/commonUtils"; +} from "../debug/utility"; describe("Debug V3 custom-copilot-rag TypeScript template", () => { const testFolder = getTestFolder(); diff --git a/packages/tests/src/e2e/teamsApp/basicBot.tests.ts b/packages/tests/src/e2e/teamsApp/basicBot.tests.ts new file mode 100644 index 00000000000..48f936895fc --- /dev/null +++ b/packages/tests/src/e2e/teamsApp/basicBot.tests.ts @@ -0,0 +1,196 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +/** + * @author Ning Tang + */ + +import { it } from "@microsoft/extra-shot-mocha"; +import MockAzureAccountProvider from "@microsoft/m365agentstoolkit-cli/src/commonlib/azureLoginUserPassword"; +import { AzureScopes, environmentNameManager } from "@microsoft/teamsfx-core"; +import { assert } from "chai"; +import fs from "fs-extra"; +import path from "path"; +import { CliHelper } from "../../commonlib/cliHelper"; +import { EnvConstants } from "../../commonlib/constants"; +import { + getResourceGroupNameFromResourceId, + getSiteNameFromResourceId, + getWebappSettings, +} from "../../commonlib/utilities"; +import { Capability } from "../../utils/constants"; +import { + cleanUpLocalProject, + createResourceGroup, + deleteResourceGroupByName, + getSubscriptionId, + getTestFolder, + getUniqueAppName, + readContextMultiEnvV3, +} from "../commonUtils"; +import { + deleteAadAppByClientId, + deleteBot, + deleteTeamsApp, + getAadAppByClientId, + getBot, + getTeamsApp, +} from "../debug/utility"; + +describe("Basic Bot", function () { + const testFolder = getTestFolder(); + const subscription = getSubscriptionId(); + const appName = getUniqueAppName(); + const resourceGroupName = `${appName}-rg`; + const projectPath = path.resolve(testFolder, appName); + const envName = environmentNameManager.getDefaultEnvName(); + + after(async () => { + // clean up + let context = await readContextMultiEnvV3(projectPath, "local"); + if (context?.TEAMS_APP_ID) { + await deleteTeamsApp(context.TEAMS_APP_ID); + } + if (context?.BOT_ID) { + await deleteBot(context.BOT_ID); + await deleteAadAppByClientId(context.BOT_ID); + } + + context = await readContextMultiEnvV3(projectPath, "dev"); + if (context?.TEAMS_APP_ID) { + await deleteTeamsApp(context.TEAMS_APP_ID); + } + await deleteResourceGroupByName(resourceGroupName); + await cleanUpLocalProject(projectPath); + }); + + it( + "typescript template", + { + testPlanCaseId: 17449538, + author: "Ning.Tang@microsoft.com", + }, + async function () { + // Scaffold + await CliHelper.createProjectWithCapability( + appName, + testFolder, + Capability.Bot, + process.env, + `--programming-language typescript` + ); + + // Validate Scaffold + const indexFile = path.join(projectPath, "index.ts"); + fs.access(indexFile, fs.constants.F_OK, (err) => { + assert.notExists(err, "index.ts should exist"); + }); + + // Local Debug (Provision) + await CliHelper.provisionProject(projectPath, "", "local", { + ...process.env, + BOT_DOMAIN: "test.ngrok.io", + BOT_ENDPOINT: "https://test.ngrok.io", + }); + console.log(`[Successfully] provision for ${projectPath}`); + + let context = await readContextMultiEnvV3(projectPath, "local"); + assert.isDefined(context, "local env file should exist"); + + // validate aad + assert.isUndefined(context.AAD_APP_OBJECT_ID, "AAD should not exist"); + + // validate teams app + assert.isDefined(context.TEAMS_APP_ID, "teams app id should be defined"); + const teamsApp = await getTeamsApp(context.TEAMS_APP_ID); + assert.equal(teamsApp?.teamsAppId, context.TEAMS_APP_ID); + + // validate bot + assert.isDefined(context.BOT_ID); + assert.isNotEmpty(context.BOT_ID); + const aadApp = await getAadAppByClientId(context.BOT_ID); + assert.isDefined(aadApp); + assert.equal(aadApp?.appId, context.BOT_ID); + const bot = await getBot(context.BOT_ID); + assert.equal(bot?.botId, context.BOT_ID); + assert.equal( + bot?.messagingEndpoint, + "https://test.ngrok.io/api/messages" + ); + + // Local Debug (Deploy) + await CliHelper.deployAll(projectPath, "", "local"); + console.log(`[Successfully] deploy for ${projectPath}`); + + context = await readContextMultiEnvV3(projectPath, "local"); + assert.isDefined(context); + + // validate .localConfigs + assert.isTrue( + await fs.pathExists(path.join(projectPath, ".localConfigs")), + ".localConfigs should exist" + ); + + // Remote Provision + const result = await createResourceGroup(resourceGroupName, "westus"); + assert.isTrue( + result, + `failed to create resource group: ${resourceGroupName}` + ); + + await CliHelper.provisionProject(projectPath, "", "dev", { + ...process.env, + AZURE_RESOURCE_GROUP_NAME: resourceGroupName, + }); + + context = await readContextMultiEnvV3(projectPath, envName); + assert.exists(context, "env file should exist"); + + // validate teams app + assert.isDefined(context.TEAMS_APP_ID); + const remoteTeamsApp = await getTeamsApp(context.TEAMS_APP_ID); + assert.equal(remoteTeamsApp?.teamsAppId, context.TEAMS_APP_ID); + + const appServiceResourceId = + context[EnvConstants.BOT_AZURE_APP_SERVICE_RESOURCE_ID]; + assert.exists( + appServiceResourceId, + "Azure App Service resource ID should exist" + ); + + const tokenProvider = MockAzureAccountProvider; + const tokenCredential = await tokenProvider.getIdentityCredentialAsync(); + const token = (await tokenCredential?.getToken(AzureScopes))?.token; + assert.exists(token); + + const response = await getWebappSettings( + subscription, + getResourceGroupNameFromResourceId(appServiceResourceId), + getSiteNameFromResourceId(appServiceResourceId), + token as string + ); + assert.exists(response, "Web app settings should exist"); + assert.equal( + response["WEBSITE_NODE_DEFAULT_VERSION"], + "~20", + "Node version should be 20" + ); + assert.equal( + response["WEBSITE_RUN_FROM_PACKAGE"], + "1", + "Run from package should be 1" + ); + assert.equal( + response["RUNNING_ON_AZURE"], + "1", + "Running on azure should be 1" + ); + + // Remote Deploy + await CliHelper.deployAll(projectPath); + + // Validate Deploy + context = await readContextMultiEnvV3(projectPath, envName); + assert.exists(context, "env file should exist"); + } + ); +}); diff --git a/packages/tests/src/e2e/teamsApp/basicMessageExtension.tests.ts b/packages/tests/src/e2e/teamsApp/basicMessageExtension.tests.ts new file mode 100644 index 00000000000..f08277245a5 --- /dev/null +++ b/packages/tests/src/e2e/teamsApp/basicMessageExtension.tests.ts @@ -0,0 +1,196 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +/** + * @author Ning Tang + */ + +import { it } from "@microsoft/extra-shot-mocha"; +import MockAzureAccountProvider from "@microsoft/m365agentstoolkit-cli/src/commonlib/azureLoginUserPassword"; +import { AzureScopes, environmentNameManager } from "@microsoft/teamsfx-core"; +import { assert } from "chai"; +import fs from "fs-extra"; +import path from "path"; +import { CliHelper } from "../../commonlib/cliHelper"; +import { EnvConstants } from "../../commonlib/constants"; +import { + getResourceGroupNameFromResourceId, + getSiteNameFromResourceId, + getWebappSettings, +} from "../../commonlib/utilities"; +import { Capability } from "../../utils/constants"; +import { + cleanUpLocalProject, + createResourceGroup, + deleteResourceGroupByName, + getSubscriptionId, + getTestFolder, + getUniqueAppName, + readContextMultiEnvV3, +} from "../commonUtils"; +import { + deleteAadAppByClientId, + deleteBot, + deleteTeamsApp, + getAadAppByClientId, + getBot, + getTeamsApp, +} from "../debug/utility"; + +describe("Basic Message Extension", function () { + const testFolder = getTestFolder(); + const subscription = getSubscriptionId(); + const appName = getUniqueAppName(); + const resourceGroupName = `${appName}-rg`; + const projectPath = path.resolve(testFolder, appName); + const envName = environmentNameManager.getDefaultEnvName(); + + after(async () => { + // clean up + let context = await readContextMultiEnvV3(projectPath, "local"); + if (context?.TEAMS_APP_ID) { + await deleteTeamsApp(context.TEAMS_APP_ID); + } + if (context?.BOT_ID) { + await deleteBot(context.BOT_ID); + await deleteAadAppByClientId(context.BOT_ID); + } + + context = await readContextMultiEnvV3(projectPath, "dev"); + if (context?.TEAMS_APP_ID) { + await deleteTeamsApp(context.TEAMS_APP_ID); + } + await deleteResourceGroupByName(resourceGroupName); + await cleanUpLocalProject(projectPath); + }); + + it( + "typescript template", + { + testPlanCaseId: 17449538, + author: "Ning.Tang@microsoft.com", + }, + async function () { + // Scaffold + await CliHelper.createProjectWithCapability( + appName, + testFolder, + Capability.MessageExtension, + process.env, + `--programming-language typescript` + ); + + // Validate Scaffold + const indexFile = path.join(projectPath, "src", "index.ts"); + fs.access(indexFile, fs.constants.F_OK, (err) => { + assert.notExists(err, "index.ts should exist"); + }); + + // Local Debug (Provision) + await CliHelper.provisionProject(projectPath, "", "local", { + ...process.env, + BOT_DOMAIN: "test.ngrok.io", + BOT_ENDPOINT: "https://test.ngrok.io", + }); + console.log(`[Successfully] provision for ${projectPath}`); + + let context = await readContextMultiEnvV3(projectPath, "local"); + assert.isDefined(context, "local env file should exist"); + + // validate aad + assert.isUndefined(context.AAD_APP_OBJECT_ID, "AAD should not exist"); + + // validate teams app + assert.isDefined(context.TEAMS_APP_ID, "teams app id should be defined"); + const teamsApp = await getTeamsApp(context.TEAMS_APP_ID); + assert.equal(teamsApp?.teamsAppId, context.TEAMS_APP_ID); + + // validate bot + assert.isDefined(context.BOT_ID); + assert.isNotEmpty(context.BOT_ID); + const aadApp = await getAadAppByClientId(context.BOT_ID); + assert.isDefined(aadApp); + assert.equal(aadApp?.appId, context.BOT_ID); + const bot = await getBot(context.BOT_ID); + assert.equal(bot?.botId, context.BOT_ID); + assert.equal( + bot?.messagingEndpoint, + "https://test.ngrok.io/api/messages" + ); + + // Local Debug (Deploy) + await CliHelper.deployAll(projectPath, "", "local"); + console.log(`[Successfully] deploy for ${projectPath}`); + + context = await readContextMultiEnvV3(projectPath, "local"); + assert.isDefined(context); + + // validate .localConfigs + assert.isTrue( + await fs.pathExists(path.join(projectPath, ".localConfigs")), + ".localConfigs should exist" + ); + + // Remote Provision + const result = await createResourceGroup(resourceGroupName, "westus"); + assert.isTrue( + result, + `failed to create resource group: ${resourceGroupName}` + ); + + await CliHelper.provisionProject(projectPath, "", "dev", { + ...process.env, + AZURE_RESOURCE_GROUP_NAME: resourceGroupName, + }); + + context = await readContextMultiEnvV3(projectPath, envName); + assert.exists(context, "env file should exist"); + + // validate teams app + assert.isDefined(context.TEAMS_APP_ID); + const remoteTeamsApp = await getTeamsApp(context.TEAMS_APP_ID); + assert.equal(remoteTeamsApp?.teamsAppId, context.TEAMS_APP_ID); + + const appServiceResourceId = + context[EnvConstants.BOT_AZURE_APP_SERVICE_RESOURCE_ID]; + assert.exists( + appServiceResourceId, + "Azure App Service resource ID should exist" + ); + + const tokenProvider = MockAzureAccountProvider; + const tokenCredential = await tokenProvider.getIdentityCredentialAsync(); + const token = (await tokenCredential?.getToken(AzureScopes))?.token; + assert.exists(token); + + const response = await getWebappSettings( + subscription, + getResourceGroupNameFromResourceId(appServiceResourceId), + getSiteNameFromResourceId(appServiceResourceId), + token as string + ); + assert.exists(response, "Web app settings should exist"); + assert.equal( + response["WEBSITE_NODE_DEFAULT_VERSION"], + "~20", + "Node version should be 20" + ); + assert.equal( + response["WEBSITE_RUN_FROM_PACKAGE"], + "1", + "Run from package should be 1" + ); + assert.equal( + response["RUNNING_ON_AZURE"], + "1", + "Running on azure should be 1" + ); + + // Remote Deploy + await CliHelper.deployAll(projectPath); + + // Validate Deploy + context = await readContextMultiEnvV3(projectPath, envName); + assert.exists(context, "env file should exist"); + } + ); +}); diff --git a/packages/tests/src/e2e/teamsApp/basicTab.tests.ts b/packages/tests/src/e2e/teamsApp/basicTab.tests.ts index fbf789b2119..f0df0906a6d 100644 --- a/packages/tests/src/e2e/teamsApp/basicTab.tests.ts +++ b/packages/tests/src/e2e/teamsApp/basicTab.tests.ts @@ -20,15 +20,15 @@ import { } from "../../commonlib/utilities"; import { Capability } from "../../utils/constants"; import { - cleanUp, + cleanUpLocalProject, createResourceGroup, + deleteResourceGroupByName, getSubscriptionId, getTestFolder, getUniqueAppName, readContextMultiEnvV3, - removeTeamsAppExtendToM365, } from "../commonUtils"; -import { getTeamsApp } from "../debug/utility"; +import { deleteTeamsApp, getTeamsApp } from "../debug/utility"; describe("Basic Tab", function () { const testFolder = getTestFolder(); @@ -39,7 +39,18 @@ describe("Basic Tab", function () { const envName = environmentNameManager.getDefaultEnvName(); afterEach(async () => { - await cleanUp(appName, projectPath, false, false, false); + // clean up + let context = await readContextMultiEnvV3(projectPath, "local"); + if (context?.TEAMS_APP_ID) { + await deleteTeamsApp(context.TEAMS_APP_ID); + } + + context = await readContextMultiEnvV3(projectPath, "dev"); + if (context?.TEAMS_APP_ID) { + await deleteTeamsApp(context.TEAMS_APP_ID); + } + await deleteResourceGroupByName(resourceGroupName); + await cleanUpLocalProject(projectPath); }); it( @@ -64,12 +75,6 @@ describe("Basic Tab", function () { assert.notExists(err, "index.ts should exist"); }); - // remove teamsApp/extendToM365 in case it fails - removeTeamsAppExtendToM365(path.join(projectPath, "m365agents.yml")); - removeTeamsAppExtendToM365( - path.join(projectPath, "m365agents.local.yml") - ); - // Local Debug (Provision) await CliHelper.provisionProject(projectPath, "", "local"); console.log(`[Successfully] provision for ${projectPath}`); @@ -85,6 +90,15 @@ describe("Basic Tab", function () { const teamsApp = await getTeamsApp(context.TEAMS_APP_ID); assert.equal(teamsApp?.teamsAppId, context.TEAMS_APP_ID); + // validate m365 + assert.isDefined( + context.M365_TITLE_ID, + "m365 title id should be defined" + ); + assert.isNotEmpty(context.M365_TITLE_ID); + assert.isDefined(context.M365_APP_ID, "m365 app id should be defined"); + assert.isNotEmpty(context.M365_APP_ID); + // Local Debug (Deploy) await CliHelper.deployAll(projectPath, "", "local"); console.log(`[Successfully] deploy for ${projectPath}`); @@ -119,6 +133,15 @@ describe("Basic Tab", function () { context = await readContextMultiEnvV3(projectPath, envName); assert.exists(context, "env file should exist"); + // validate m365 + assert.isDefined( + context.M365_TITLE_ID, + "m365 title id should be defined" + ); + assert.isNotEmpty(context.M365_TITLE_ID); + assert.isDefined(context.M365_APP_ID, "m365 app id should be defined"); + assert.isNotEmpty(context.M365_APP_ID); + const appServiceResourceId = context[EnvConstants.TAB_AZURE_APP_SERVICE_RESOURCE_ID]; assert.exists( diff --git a/packages/tests/src/e2e/bot/BotHappyPathCommon.ts b/packages/tests/src/e2e/vs/BotHappyPathCommon.ts similarity index 100% rename from packages/tests/src/e2e/bot/BotHappyPathCommon.ts rename to packages/tests/src/e2e/vs/BotHappyPathCommon.ts diff --git a/packages/tests/src/e2e/vs/EchoBotHappyPath.dotnet.tests.ts b/packages/tests/src/e2e/vs/EchoBotHappyPath.dotnet.tests.ts index bd7505ded88..6b85d7261b6 100644 --- a/packages/tests/src/e2e/vs/EchoBotHappyPath.dotnet.tests.ts +++ b/packages/tests/src/e2e/vs/EchoBotHappyPath.dotnet.tests.ts @@ -7,7 +7,7 @@ import { it } from "@microsoft/extra-shot-mocha"; import { Runtime } from "../../commonlib/constants"; -import { happyPathTest } from "../bot/BotHappyPathCommon"; +import { happyPathTest } from "./BotHappyPathCommon"; describe("Remote happy path for echo bot dotnet", () => { it( From 47ff10d05ce14d78f8983b95a291fa18f37e55c5 Mon Sep 17 00:00:00 2001 From: Yuqi Zhou <86260893+yuqizhou77@users.noreply.github.com> Date: Tue, 23 Sep 2025 20:20:09 +0800 Subject: [PATCH 15/24] fix: template typos (#14583) * fix: template typos * fix: template typos --- .../custom-copilot-basic/appsettings.Playground.json.tpl | 2 +- .../Controllers/Controller.cs.tpl | 4 ++-- .../Controllers/Controller.cs.tpl | 4 ++-- .../csharp/custom-copilot-rag-customize/MyDataSource.cs.tpl | 2 +- .../custom-copilot-rag-customize/Utils/TextUtils.cs.tpl | 1 + .../appsettings.Playground.json.tpl | 2 +- .../appsettings.Playground.json.tpl | 2 +- 7 files changed, 9 insertions(+), 8 deletions(-) diff --git a/templates/vs/csharp/custom-copilot-basic/appsettings.Playground.json.tpl b/templates/vs/csharp/custom-copilot-basic/appsettings.Playground.json.tpl index a5599fbdc81..49aef240a4b 100644 --- a/templates/vs/csharp/custom-copilot-basic/appsettings.Playground.json.tpl +++ b/templates/vs/csharp/custom-copilot-basic/appsettings.Playground.json.tpl @@ -18,7 +18,7 @@ }, {{#useOpenAI}} "OpenAI": { - "ApiKey": "{{{originalOpenAIKey}}}", + "ApiKey": "{{{originalOpenAIKey}}}", "Model": "gpt-3.5-turbo" } {{/useOpenAI}} diff --git a/templates/vs/csharp/custom-copilot-rag-azure-ai-search/Controllers/Controller.cs.tpl b/templates/vs/csharp/custom-copilot-rag-azure-ai-search/Controllers/Controller.cs.tpl index c7657c101a8..b7df701747c 100644 --- a/templates/vs/csharp/custom-copilot-rag-azure-ai-search/Controllers/Controller.cs.tpl +++ b/templates/vs/csharp/custom-copilot-rag-azure-ai-search/Controllers/Controller.cs.tpl @@ -11,10 +11,10 @@ using Microsoft.Teams.Apps.Annotations; namespace {{SafeProjectName}}.Controllers { [TeamsController] - public class Controller(OpenAIChatPrompt _prompt) + public class Controller(OpenAIChatPrompt _prompt, AzureAISearchDataSource dataSource) { [Message] - public async Task OnMessage(IContext context, AzureAISearchDataSource dataSource) + public async Task OnMessage(IContext context) { var state = State.From(context); var text = TextUtils.StripMentionsText(context.Activity); diff --git a/templates/vs/csharp/custom-copilot-rag-customize/Controllers/Controller.cs.tpl b/templates/vs/csharp/custom-copilot-rag-customize/Controllers/Controller.cs.tpl index 166c019297a..c4ec4f19baf 100644 --- a/templates/vs/csharp/custom-copilot-rag-customize/Controllers/Controller.cs.tpl +++ b/templates/vs/csharp/custom-copilot-rag-customize/Controllers/Controller.cs.tpl @@ -11,10 +11,10 @@ using Microsoft.Teams.Apps.Annotations; namespace {{SafeProjectName}}.Controllers { [TeamsController] - public class Controller(OpenAIChatPrompt _prompt) + public class Controller(OpenAIChatPrompt _prompt, MyDataSource dataSource) { [Message] - public async Task OnMessage(IContext context, MyDataSource dataSource) + public async Task OnMessage(IContext context) { var state = State.From(context); var text = TextUtils.StripMentionsText(context.Activity); diff --git a/templates/vs/csharp/custom-copilot-rag-customize/MyDataSource.cs.tpl b/templates/vs/csharp/custom-copilot-rag-customize/MyDataSource.cs.tpl index 724027dcdcd..262ebc3f1dc 100644 --- a/templates/vs/csharp/custom-copilot-rag-customize/MyDataSource.cs.tpl +++ b/templates/vs/csharp/custom-copilot-rag-customize/MyDataSource.cs.tpl @@ -1,4 +1,4 @@ -namespace {{SafeProjectName}}; +namespace {{SafeProjectName}} { public class MyDataSource { diff --git a/templates/vs/csharp/custom-copilot-rag-customize/Utils/TextUtils.cs.tpl b/templates/vs/csharp/custom-copilot-rag-customize/Utils/TextUtils.cs.tpl index 7d5a9579596..9f4a0308034 100644 --- a/templates/vs/csharp/custom-copilot-rag-customize/Utils/TextUtils.cs.tpl +++ b/templates/vs/csharp/custom-copilot-rag-customize/Utils/TextUtils.cs.tpl @@ -2,6 +2,7 @@ using Microsoft.Teams.Api.Activities; using Microsoft.Teams.Api.Entities; namespace {{SafeProjectName}}.Utils +{ public static class TextUtils { public static string StripMentionsText(MessageActivity activity) diff --git a/templates/vs/csharp/custom-copilot-rag-customize/appsettings.Playground.json.tpl b/templates/vs/csharp/custom-copilot-rag-customize/appsettings.Playground.json.tpl index a5599fbdc81..49aef240a4b 100644 --- a/templates/vs/csharp/custom-copilot-rag-customize/appsettings.Playground.json.tpl +++ b/templates/vs/csharp/custom-copilot-rag-customize/appsettings.Playground.json.tpl @@ -18,7 +18,7 @@ }, {{#useOpenAI}} "OpenAI": { - "ApiKey": "{{{originalOpenAIKey}}}", + "ApiKey": "{{{originalOpenAIKey}}}", "Model": "gpt-3.5-turbo" } {{/useOpenAI}} diff --git a/templates/vs/csharp/teams-agent-with-data-custom-api-v2/appsettings.Playground.json.tpl b/templates/vs/csharp/teams-agent-with-data-custom-api-v2/appsettings.Playground.json.tpl index a5599fbdc81..49aef240a4b 100644 --- a/templates/vs/csharp/teams-agent-with-data-custom-api-v2/appsettings.Playground.json.tpl +++ b/templates/vs/csharp/teams-agent-with-data-custom-api-v2/appsettings.Playground.json.tpl @@ -18,7 +18,7 @@ }, {{#useOpenAI}} "OpenAI": { - "ApiKey": "{{{originalOpenAIKey}}}", + "ApiKey": "{{{originalOpenAIKey}}}", "Model": "gpt-3.5-turbo" } {{/useOpenAI}} From d44a348b904b0b58bf2aa5c692aa34360fd9db7b Mon Sep 17 00:00:00 2001 From: Yuqi Zhou Date: Tue, 23 Sep 2025 22:02:50 +0800 Subject: [PATCH 16/24] refactor: csharp AI V2 readme --- .../.{{NewProjectTypeName}}/README.md.tpl | 2 +- .../.{{NewProjectTypeName}}/README.md.tpl | 2 +- .../custom-copilot-basic/.{{NewProjectTypeName}}/README.md.tpl | 2 +- .../.{{NewProjectTypeName}}/README.md.tpl | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/vs/csharp/custom-copilot-assistant-assistants-api/.{{NewProjectTypeName}}/README.md.tpl b/templates/vs/csharp/custom-copilot-assistant-assistants-api/.{{NewProjectTypeName}}/README.md.tpl index 9d75ff4ffe4..a18680e8d4a 100644 --- a/templates/vs/csharp/custom-copilot-assistant-assistants-api/.{{NewProjectTypeName}}/README.md.tpl +++ b/templates/vs/csharp/custom-copilot-assistant-assistants-api/.{{NewProjectTypeName}}/README.md.tpl @@ -109,7 +109,7 @@ You can follow [Build an AI Agent in Teams](https://aka.ms/teamsfx-ai-agent) to - [Add functions](https://aka.ms/teamsfx-ai-agent#add-functions-with-assistants-api) ## Additional information and references -- [Teams AI library](https://aka.ms/teams-ai-library) +- [Teams AI library V2](https://aka.ms/teams-ai-library-v2) - [Microsoft 365 Agents Toolkit Documentations](https://docs.microsoft.com/microsoftteams/platform/toolkit/teams-toolkit-fundamentals) - [Microsoft 365 Agents Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) - [Microsoft 365 Agents Toolkit Samples](https://github.com/OfficeDev/TeamsFx-Samples) diff --git a/templates/vs/csharp/custom-copilot-assistant-new/.{{NewProjectTypeName}}/README.md.tpl b/templates/vs/csharp/custom-copilot-assistant-new/.{{NewProjectTypeName}}/README.md.tpl index 46b8d94ed41..9f73147a15a 100644 --- a/templates/vs/csharp/custom-copilot-assistant-new/.{{NewProjectTypeName}}/README.md.tpl +++ b/templates/vs/csharp/custom-copilot-assistant-new/.{{NewProjectTypeName}}/README.md.tpl @@ -76,7 +76,7 @@ The app template is built using the Teams AI library, which provides the capabil You can follow [Get started with Teams AI library](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/teams%20conversational%20ai/how-conversation-ai-get-started) to extend the agent template with more AI capabilities. ## Additional information and references -- [Teams AI library](https://aka.ms/teams-ai-library) +- [Teams AI library V2](https://aka.ms/teams-ai-library-v2) - [Microsoft 365 Agents Toolkit Documentations](https://docs.microsoft.com/microsoftteams/platform/toolkit/teams-toolkit-fundamentals) - [Microsoft 365 Agents Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) - [Microsoft 365 Agents Toolkit Samples](https://github.com/OfficeDev/TeamsFx-Samples) diff --git a/templates/vs/csharp/custom-copilot-basic/.{{NewProjectTypeName}}/README.md.tpl b/templates/vs/csharp/custom-copilot-basic/.{{NewProjectTypeName}}/README.md.tpl index 581ec36ab63..c9b363431f8 100644 --- a/templates/vs/csharp/custom-copilot-basic/.{{NewProjectTypeName}}/README.md.tpl +++ b/templates/vs/csharp/custom-copilot-basic/.{{NewProjectTypeName}}/README.md.tpl @@ -71,7 +71,7 @@ The app template is built using the Teams AI library, which provides the capabil You can follow [Get started with Teams AI library](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/teams%20conversational%20ai/how-conversation-ai-get-started) to extend the agent template with more AI capabilities. ## Additional information and references -- [Teams AI library](https://aka.ms/teams-ai-library) +- [Teams AI library V2](https://aka.ms/teams-ai-library-v2) - [Microsoft 365 Agents Toolkit Documentations](https://docs.microsoft.com/microsoftteams/platform/toolkit/teams-toolkit-fundamentals) - [Microsoft 365 Agents Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) - [Microsoft 365 Agents Toolkit Samples](https://github.com/OfficeDev/TeamsFx-Samples) diff --git a/templates/vs/csharp/custom-copilot-rag-customize/.{{NewProjectTypeName}}/README.md.tpl b/templates/vs/csharp/custom-copilot-rag-customize/.{{NewProjectTypeName}}/README.md.tpl index 7ff49f11674..1cb897721f8 100644 --- a/templates/vs/csharp/custom-copilot-rag-customize/.{{NewProjectTypeName}}/README.md.tpl +++ b/templates/vs/csharp/custom-copilot-rag-customize/.{{NewProjectTypeName}}/README.md.tpl @@ -3,7 +3,7 @@ This app template showcases how to build one of the most powerful applications enabled by LLM - sophisticated question-answering (Q&A) chat bots that can answer questions about specific source information right in the Microsoft Teams. This app template also demonstrates usage of techniques like: - [Retrieval Augmented Generation](https://python.langchain.com/docs/use_cases/question_answering/#what-is-rag), or RAG. -- [Teams AI Library](https://learn.microsoft.com/microsoftteams/platform/bots/how-to/teams%20conversational%20ai/teams-conversation-ai-overview) +- [Teams AI library V2](https://aka.ms/teams-ai-library-v2) ## Get started with the template From a343856e59548091d431cd242861dbf080214d5b Mon Sep 17 00:00:00 2001 From: Yuqi Zhou Date: Wed, 24 Sep 2025 07:40:23 +0800 Subject: [PATCH 17/24] refactor: csharp AI V2 readme --- .../.{{NewProjectTypeName}}/README.md.tpl | 2 +- .../.{{NewProjectTypeName}}/README.md.tpl | 2 +- .../.{{NewProjectTypeName}}/README.md.tpl | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/vs/csharp/custom-copilot-assistant-assistants-api/.{{NewProjectTypeName}}/README.md.tpl b/templates/vs/csharp/custom-copilot-assistant-assistants-api/.{{NewProjectTypeName}}/README.md.tpl index a18680e8d4a..9d75ff4ffe4 100644 --- a/templates/vs/csharp/custom-copilot-assistant-assistants-api/.{{NewProjectTypeName}}/README.md.tpl +++ b/templates/vs/csharp/custom-copilot-assistant-assistants-api/.{{NewProjectTypeName}}/README.md.tpl @@ -109,7 +109,7 @@ You can follow [Build an AI Agent in Teams](https://aka.ms/teamsfx-ai-agent) to - [Add functions](https://aka.ms/teamsfx-ai-agent#add-functions-with-assistants-api) ## Additional information and references -- [Teams AI library V2](https://aka.ms/teams-ai-library-v2) +- [Teams AI library](https://aka.ms/teams-ai-library) - [Microsoft 365 Agents Toolkit Documentations](https://docs.microsoft.com/microsoftteams/platform/toolkit/teams-toolkit-fundamentals) - [Microsoft 365 Agents Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) - [Microsoft 365 Agents Toolkit Samples](https://github.com/OfficeDev/TeamsFx-Samples) diff --git a/templates/vs/csharp/custom-copilot-assistant-new/.{{NewProjectTypeName}}/README.md.tpl b/templates/vs/csharp/custom-copilot-assistant-new/.{{NewProjectTypeName}}/README.md.tpl index 9f73147a15a..46b8d94ed41 100644 --- a/templates/vs/csharp/custom-copilot-assistant-new/.{{NewProjectTypeName}}/README.md.tpl +++ b/templates/vs/csharp/custom-copilot-assistant-new/.{{NewProjectTypeName}}/README.md.tpl @@ -76,7 +76,7 @@ The app template is built using the Teams AI library, which provides the capabil You can follow [Get started with Teams AI library](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/teams%20conversational%20ai/how-conversation-ai-get-started) to extend the agent template with more AI capabilities. ## Additional information and references -- [Teams AI library V2](https://aka.ms/teams-ai-library-v2) +- [Teams AI library](https://aka.ms/teams-ai-library) - [Microsoft 365 Agents Toolkit Documentations](https://docs.microsoft.com/microsoftteams/platform/toolkit/teams-toolkit-fundamentals) - [Microsoft 365 Agents Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) - [Microsoft 365 Agents Toolkit Samples](https://github.com/OfficeDev/TeamsFx-Samples) diff --git a/templates/vs/csharp/custom-copilot-rag-azure-ai-search/.{{NewProjectTypeName}}/README.md.tpl b/templates/vs/csharp/custom-copilot-rag-azure-ai-search/.{{NewProjectTypeName}}/README.md.tpl index 6f84980f085..074407e88cc 100644 --- a/templates/vs/csharp/custom-copilot-rag-azure-ai-search/.{{NewProjectTypeName}}/README.md.tpl +++ b/templates/vs/csharp/custom-copilot-rag-azure-ai-search/.{{NewProjectTypeName}}/README.md.tpl @@ -3,7 +3,7 @@ This app template showcases how to build one of the most powerful applications enabled by LLM - sophisticated question-answering (Q&A) agent that can answer questions about specific source information right in the Microsoft Teams. This app template also demonstrates usage of techniques like: - [Retrieval Augmented Generation](https://python.langchain.com/docs/use_cases/question_answering/#what-is-rag), or RAG. -- [Teams AI Library](https://aka.ms/teams-ai-library-v2) +- [Teams AI Library V2](https://aka.ms/teams-ai-library-v2) ## Get started with the template From 780fbce8809f050218e845bbf1f5af18cb32af4e Mon Sep 17 00:00:00 2001 From: Qinzhou Xu Date: Wed, 24 Sep 2025 09:48:44 +0800 Subject: [PATCH 18/24] fix: cherry-pick decryption error across projects (#14582) * fix: cherry-pick decryption error across projects * fix: add ut --- packages/fx-core/src/core/crypto.ts | 14 ++- packages/fx-core/tests/core/crypto.test.ts | 138 +++++++++++++++++++++ 2 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 packages/fx-core/tests/core/crypto.test.ts diff --git a/packages/fx-core/src/core/crypto.ts b/packages/fx-core/src/core/crypto.ts index cfc5fab4118..37baa9bc6fb 100644 --- a/packages/fx-core/src/core/crypto.ts +++ b/packages/fx-core/src/core/crypto.ts @@ -6,14 +6,16 @@ import Cryptr from "cryptr"; export class LocalCrypto implements CryptoProvider { private cryptr: Cryptr; + private fixedCryptr: Cryptr; private prefix = "crypto_"; constructor(projectId: string) { this.cryptr = new Cryptr(projectId + "_teamsfx"); + this.fixedCryptr = new Cryptr("teamsfx_global_key"); } public encrypt(plaintext: string): Result { - return ok(this.prefix + this.cryptr.encrypt(plaintext)); + return ok(this.prefix + this.fixedCryptr.encrypt(plaintext)); } public decrypt(ciphertext: string): Result { @@ -21,11 +23,15 @@ export class LocalCrypto implements CryptoProvider { // legacy raw secret string return ok(ciphertext); } + const encryptedData = ciphertext.substr(this.prefix.length); try { - return ok(this.cryptr.decrypt(ciphertext.substr(this.prefix.length))); + return ok(this.fixedCryptr.decrypt(encryptedData)); } catch (e) { - // ciphertext is broken - return err(new SystemError("Core", "DecryptionError", "Cipher text is broken")); + try { + return ok(this.cryptr.decrypt(encryptedData)); + } catch (e2) { + return err(new SystemError("Core", "DecryptionError", "Cipher text is broken")); + } } } } diff --git a/packages/fx-core/tests/core/crypto.test.ts b/packages/fx-core/tests/core/crypto.test.ts new file mode 100644 index 00000000000..555e96a331f --- /dev/null +++ b/packages/fx-core/tests/core/crypto.test.ts @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { assert } from "chai"; +import "mocha"; +import { createSandbox } from "sinon"; +import Cryptr from "cryptr"; +import { LocalCrypto } from "../../src/core/crypto"; +import { SystemError } from "@microsoft/teamsfx-api"; + +describe("LocalCrypto", () => { + const sandbox = createSandbox(); + const testProjectId = "test-project-123"; + const testPlaintext = "sensitive-data-to-encrypt"; + const prefix = "crypto_"; + + let localCrypto: LocalCrypto; + let fixedCryptr: Cryptr; + let projectCryptr: Cryptr; + + beforeEach(() => { + localCrypto = new LocalCrypto(testProjectId); + fixedCryptr = new Cryptr("teamsfx_global_key"); + projectCryptr = new Cryptr(testProjectId + "_teamsfx"); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe("encrypt", () => { + it("should encrypt plaintext with fixed global key and add prefix", () => { + const result = localCrypto.encrypt(testPlaintext); + + assert.isTrue(result.isOk()); + if (result.isOk()) { + const encrypted = result.value; + assert.isTrue(encrypted.startsWith(prefix)); + + const encryptedData = encrypted.substr(prefix.length); + const decrypted = fixedCryptr.decrypt(encryptedData); + assert.equal(decrypted, testPlaintext); + } + }); + }); + + describe("decrypt", () => { + it("should decrypt strings encrypted with fixed global key (new encryption)", () => { + const encrypted = fixedCryptr.encrypt(testPlaintext); + const ciphertext = prefix + encrypted; + + const result = localCrypto.decrypt(ciphertext); + + assert.isTrue(result.isOk()); + if (result.isOk()) { + assert.equal(result.value, testPlaintext); + } + }); + + it("should decrypt strings encrypted with project-specific key (old encryption fallback)", () => { + const encrypted = projectCryptr.encrypt(testPlaintext); + const ciphertext = prefix + encrypted; + + const result = localCrypto.decrypt(ciphertext); + + assert.isTrue(result.isOk()); + if (result.isOk()) { + assert.equal(result.value, testPlaintext); + } + }); + + it("should return error when both fixed and project cryptr fail to decrypt", () => { + const invalidCiphertext = prefix + "invalid-cipher-text-that-cannot-be-decrypted"; + + const result = localCrypto.decrypt(invalidCiphertext); + + assert.isTrue(result.isErr()); + if (result.isErr()) { + assert.instanceOf(result.error, SystemError); + assert.equal(result.error.source, "Core"); + assert.equal(result.error.name, "DecryptionError"); + assert.equal(result.error.message, "Cipher text is broken"); + } + }); + + it("should successfully encrypt and decrypt data (round trip)", () => { + const encryptResult = localCrypto.encrypt(testPlaintext); + assert.isTrue(encryptResult.isOk()); + + if (encryptResult.isOk()) { + const decryptResult = localCrypto.decrypt(encryptResult.value); + assert.isTrue(decryptResult.isOk()); + + if (decryptResult.isOk()) { + assert.equal(decryptResult.value, testPlaintext); + } + } + }); + + it("should handle cross-project compatibility for new encryption", () => { + const crypto1 = new LocalCrypto("project1"); + const crypto2 = new LocalCrypto("project2"); + + // New encryption uses fixed global key, so it works across different project IDs + const encryptResult = crypto1.encrypt(testPlaintext); + assert.isTrue(encryptResult.isOk()); + + if (encryptResult.isOk()) { + const decryptResult = crypto2.decrypt(encryptResult.value); + assert.isTrue(decryptResult.isOk()); + + if (decryptResult.isOk()) { + assert.equal(decryptResult.value, testPlaintext); + } + } + }); + + it("should not decrypt legacy data from different project IDs", () => { + const project1Crypto = new LocalCrypto("project1"); + const project2Crypto = new LocalCrypto("project2"); + const project1Cryptr = new Cryptr("project1_teamsfx"); + + // Create legacy encrypted data with project1 key + const legacyEncrypted = prefix + project1Cryptr.encrypt(testPlaintext); + + // project1 crypto should decrypt it (fallback to project-specific key) + const project1Result = project1Crypto.decrypt(legacyEncrypted); + assert.isTrue(project1Result.isOk()); + if (project1Result.isOk()) { + assert.equal(project1Result.value, testPlaintext); + } + + // project2 crypto should fail to decrypt it (different project key) + const project2Result = project2Crypto.decrypt(legacyEncrypted); + assert.isTrue(project2Result.isErr()); + }); + }); +}); From c88eb08232468ab653d4c1bbf988377771387417 Mon Sep 17 00:00:00 2001 From: Helly Zhang <49181894+hellyzh@users.noreply.github.com> Date: Thu, 25 Sep 2025 14:50:36 +0800 Subject: [PATCH 19/24] test: upgrade dependencies of fs-extra to latest version (#14591) * test: upgrade dependencies of fs-extra to latest version * test: update display name for test accounts --- .github/workflows/ui-test.yml | 2 +- packages/tests/package.json | 4 +- packages/tests/pnpm-lock.yaml | 50 +++++++++---------------- packages/tests/src/scripts/m365Login.ts | 4 +- 4 files changed, 22 insertions(+), 38 deletions(-) diff --git a/.github/workflows/ui-test.yml b/.github/workflows/ui-test.yml index a127b299db3..1f412937a9f 100644 --- a/.github/workflows/ui-test.yml +++ b/.github/workflows/ui-test.yml @@ -194,7 +194,7 @@ jobs: M365_USERNAME_3: "test16@xxbdw.onmicrosoft.com" M365_USERNAME_4: "test17@xxbdw.onmicrosoft.com" M365_USERNAME_5: "ttktest01@xxbdw.onmicrosoft.com" - M365_DISPLAY_NAME: "ttktest" + M365_DISPLAY_NAME: "test" M365_TENANT_ID: ${{ secrets.TEST_CLEAN_TENANT_ID }} M365_COLLABORATOR: "haolong@xxbdw.onmicrosoft.com" diff --git a/packages/tests/package.json b/packages/tests/package.json index 15d205a5593..abd15deca5c 100644 --- a/packages/tests/package.json +++ b/packages/tests/package.json @@ -23,7 +23,7 @@ "@microsoft/teamsfx-core": "workspace:*", "@types/adm-zip": "0.5.5", "@types/chai": "^4.2.19", - "@types/fs-extra": "^7.0.0", + "@types/fs-extra": "^11.0.4", "@types/glob": "^7.1.3", "@types/keytar": "^4.4.2", "@types/md5": "^2.3.0", @@ -80,7 +80,7 @@ "axios": "^1.6.8", "dotenv": "^8.2.0", "form-data": "4.0.0", - "fs-extra": "^7.0.1", + "fs-extra": "^11.3.2", "is-wsl": "^2.2.0", "md5": "^2.3.0", "querystring": "^0.2.1", diff --git a/packages/tests/pnpm-lock.yaml b/packages/tests/pnpm-lock.yaml index be107a7d6e2..a6d21fb3962 100644 --- a/packages/tests/pnpm-lock.yaml +++ b/packages/tests/pnpm-lock.yaml @@ -45,8 +45,8 @@ dependencies: specifier: 4.0.0 version: 4.0.0 fs-extra: - specifier: ^7.0.1 - version: 7.0.1 + specifier: ^11.3.2 + version: 11.3.2 is-wsl: specifier: ^2.2.0 version: 2.2.0 @@ -100,8 +100,8 @@ devDependencies: specifier: ^4.2.19 version: 4.2.19 '@types/fs-extra': - specifier: ^7.0.0 - version: 7.0.0 + specifier: ^11.0.4 + version: 11.0.4 '@types/glob': specifier: ^7.1.3 version: 7.1.3 @@ -941,7 +941,7 @@ packages: clipboardy: 4.0.0 clone-deep: 4.0.1 compare-versions: 6.1.1 - fs-extra: 11.3.0 + fs-extra: 11.3.2 selenium-webdriver: 4.29.0 type-fest: 4.35.0 typescript: 5.0.4 @@ -1020,9 +1020,10 @@ packages: resolution: {integrity: sha512-jRJgpRBuY+7izT7/WNXP/LsMO9YonsstuL+xuvycDyESpoDoIAsMd7suwpB4h9oEWB+ZlPTqJJ8EHomzNhwTPQ==} dev: true - /@types/fs-extra@7.0.0: - resolution: {integrity: sha512-ndoMMbGyuToTy4qB6Lex/inR98nPiNHacsgMPvy+zqMLgSxbt8VtWpDArpGp69h1fEDQHn1KB+9DWD++wgbwYA==} + /@types/fs-extra@11.0.4: + resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} dependencies: + '@types/jsonfile': 6.1.4 '@types/node': 14.17.4 dev: true @@ -1053,6 +1054,12 @@ packages: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} dev: true + /@types/jsonfile@6.1.4: + resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} + dependencies: + '@types/node': 14.17.4 + dev: true + /@types/keytar@4.4.2: resolution: {integrity: sha512-xtQcDj9ruGnMwvSu1E2BH4SFa5Dv2PvSPd0CKEBLN5hEj/v5YpXJY+B6hAfuKIbvEomD7vJTc/P1s1xPNh2kRw==} deprecated: This is a stub types definition. keytar provides its own type definitions, so you do not need this installed. @@ -3175,23 +3182,13 @@ packages: universalify: 2.0.1 dev: true - /fs-extra@11.3.0: - resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} + /fs-extra@11.3.2: + resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==} engines: {node: '>=14.14'} dependencies: graceful-fs: 4.2.11 jsonfile: 6.1.0 universalify: 2.0.1 - dev: true - - /fs-extra@7.0.1: - resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} - engines: {node: '>=6 <7 || >=8'} - dependencies: - graceful-fs: 4.2.11 - jsonfile: 4.0.0 - universalify: 0.1.2 - dev: false /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -4092,19 +4089,12 @@ packages: resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} dev: true - /jsonfile@4.0.0: - resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} - optionalDependencies: - graceful-fs: 4.2.11 - dev: false - /jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} dependencies: universalify: 2.0.1 optionalDependencies: graceful-fs: 4.2.11 - dev: true /jsonwebtoken@9.0.2: resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} @@ -6241,15 +6231,9 @@ packages: engines: {node: '>=18'} dev: true - /universalify@0.1.2: - resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} - engines: {node: '>= 4.0.0'} - dev: false - /universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} - dev: true /update-browserslist-db@1.1.2(browserslist@4.24.4): resolution: {integrity: sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==} @@ -6335,7 +6319,7 @@ packages: commander: 12.1.0 compare-versions: 6.1.1 find-up: 7.0.0 - fs-extra: 11.3.0 + fs-extra: 11.3.2 glob: 10.4.5 got: 13.0.0 hpagent: 1.2.0 diff --git a/packages/tests/src/scripts/m365Login.ts b/packages/tests/src/scripts/m365Login.ts index b65c9195daf..de9b6ebc715 100644 --- a/packages/tests/src/scripts/m365Login.ts +++ b/packages/tests/src/scripts/m365Login.ts @@ -59,7 +59,7 @@ function getBeforeCacheAccess(accountName: string) { fs.writeFile( fileCachePath, cacheContext.tokenCache.serialize(), - (err) => { + (err: any) => { if (err) { console.error("write token fail: " + err.message); reject(); @@ -81,7 +81,7 @@ function getAfterCacheAccess(scopes: string[], accountName: string) { fs.writeFile( fileCachePath, cacheContext.tokenCache.serialize(), - (err) => { + (err: any) => { if (err) { console.error("save token fail: " + err.message); } From 5b79b9ce0108a46fbd69a44fd4e624c637145b68 Mon Sep 17 00:00:00 2001 From: HuihuiWu-Microsoft <73154171+HuihuiWu-Microsoft@users.noreply.github.com> Date: Thu, 25 Sep 2025 16:59:10 +0800 Subject: [PATCH 20/24] feat: support MFA enforcement in VSC ATK (#14573) * feat: add proposed api for mfa auth * feat: support www-authenticate request * feat: support mfa setup flow for 401 in vsc atk * feat: display friendly msg for cli since cli doesnt support mfa enforcement yet * feat: block users for vs atk since vs doesnt support mfa enforcement yet * fix: get clamis not found in challenge error * feat: use proposed apis * test: fix ut * test: fix ut * fix: specify vsc engine version * fix: ut error * fix: build break * test: fix ut * test: disable ut * test: add ut --- packages/api/src/utils/login.ts | 20 +- packages/cli/src/commonlib/azureLogin.ts | 13 +- packages/fx-core/package.json | 1 + packages/fx-core/pnpm-lock.yaml | 88 +++++++-- packages/fx-core/resource/package.nls.json | 1 + .../src/component/driver/arm/deployImpl.ts | 19 +- .../component/utils/ResourceGroupHelper.ts | 24 +-- .../src/component/utils/azureClient.ts | 48 +++++ .../src/component/utils/pipelinePolicy.ts | 46 +++++ packages/fx-core/src/error/common.ts | 11 ++ .../tests/component/util/azureClient.test.ts | 50 +++++ .../component/util/pipelinePolicy.test.ts | 81 ++++++++ .../fx-core/tests/question/question.test.ts | 5 +- packages/server/src/providers/token/azure.ts | 11 +- .../tests/providers/token/azure.test.ts | 13 ++ packages/vscode-extension/package.json | 8 +- packages/vscode-extension/pnpm-lock.yaml | 8 +- .../src/commonlib/azureLogin.ts | 28 ++- .../vscodeAzureSubscriptionProvider.ts | 30 ++- .../src/debug/officeTaskHandler.ts | 16 +- .../src/debug/teamsfxTaskHandler.ts | 28 +-- .../src/vscode.proposed.authLearnMore.d.ts | 15 ++ ...ode.proposed.authenticationChallenges.d.ts | 177 ++++++++++++++++++ .../{utils.test.ts => utils.test.ts.disable} | 0 .../vscode-extension/test/mocks/vsc/chat.ts | 4 +- .../test/officeChat/common/planner.test.ts | 2 +- ...test.ts => sampleProvider.test.ts.disable} | 0 .../common/skills/codeExplainer.test.ts | 2 +- ....test.ts => codeGenerator.test.ts.disable} | 2 +- .../common/skills/codeIssueCorrector.test.ts | 23 +-- .../officeChat/common/skills/printer.test.ts | 2 +- .../common/skills/projectCreator.test.ts | 2 +- .../officeChat/common/skills/skillset.test.ts | 2 +- .../test/officeChat/handlers.test.ts | 28 ++- 34 files changed, 686 insertions(+), 122 deletions(-) create mode 100644 packages/fx-core/src/component/utils/azureClient.ts create mode 100644 packages/fx-core/src/component/utils/pipelinePolicy.ts create mode 100644 packages/fx-core/tests/component/util/azureClient.test.ts create mode 100644 packages/fx-core/tests/component/util/pipelinePolicy.test.ts create mode 100644 packages/vscode-extension/src/vscode.proposed.authLearnMore.d.ts create mode 100644 packages/vscode-extension/src/vscode.proposed.authenticationChallenges.d.ts rename packages/vscode-extension/test/chat/{utils.test.ts => utils.test.ts.disable} (100%) rename packages/vscode-extension/test/officeChat/common/samples/{sampleProvider.test.ts => sampleProvider.test.ts.disable} (100%) rename packages/vscode-extension/test/officeChat/common/skills/{codeGenerator.test.ts => codeGenerator.test.ts.disable} (99%) diff --git a/packages/api/src/utils/login.ts b/packages/api/src/utils/login.ts index b09f5fe3e68..63a76f358c8 100644 --- a/packages/api/src/utils/login.ts +++ b/packages/api/src/utils/login.ts @@ -57,6 +57,21 @@ export type AzureCredential = certificatePath: string; }; +export interface AuthenticationWWWAuthenticateRequest { + /** + * The raw WWW-Authenticate header value that triggered this challenge. + * This will be parsed by the authentication provider to extract the necessary + * challenge information. + */ + readonly wwwAuthenticate: string; + + /** + * Optional scopes for the session. If not provided, the authentication provider + * may use default scopes or extract them from the challenge. + */ + readonly scopes?: readonly string[]; +} + /** * Difference between getAccountCredential and getIdentityCredential [Node Azure Authenticate](https://docs.microsoft.com/en-us/azure/developer/javascript/core/node-sdk-azure-authenticate) * You can search at [Azure JS SDK](https://docs.microsoft.com/en-us/javascript/api/overview/azure/?view=azure-node-latest) to see which credential you need. @@ -66,7 +81,10 @@ export interface AzureAccountProvider { * Async get identity [crendential](https://github.com/Azure/azure-sdk-for-js/blob/master/sdk/core/core-auth/src/tokenCredential.ts) * @param showDialog Control whether the UI layer displays pop-up windows. */ - getIdentityCredentialAsync(showDialog?: boolean): Promise; + getIdentityCredentialAsync( + showDialog?: boolean, + authenticationSessionRequest?: AuthenticationWWWAuthenticateRequest + ): Promise; /** * To support credential per action feature, caller can specify credential info for on demand diff --git a/packages/cli/src/commonlib/azureLogin.ts b/packages/cli/src/commonlib/azureLogin.ts index d1b8d1a945e..fb8cdfce24e 100644 --- a/packages/cli/src/commonlib/azureLogin.ts +++ b/packages/cli/src/commonlib/azureLogin.ts @@ -7,6 +7,7 @@ import { SubscriptionClient } from "@azure/arm-subscriptions"; import { AccessToken, GetTokenOptions, TokenCredential } from "@azure/core-auth"; import { LogLevel } from "@azure/msal-node"; import { + AuthenticationWWWAuthenticateRequest, AzureAccountProvider, ConfigFolderName, FxError, @@ -24,8 +25,7 @@ import { AzureScopes, isValidProjectV3, InvalidAzureSubscriptionError, - featureFlagManager, - FeatureFlags, + MFARequiredError, } from "@microsoft/teamsfx-core"; import * as fs from "fs-extra"; import * as path from "path"; @@ -177,7 +177,13 @@ export class AzureAccountManager extends login implements AzureAccountProvider { /** * Async get identity [crendential](https://github.com/Azure/azure-sdk-for-js/blob/master/sdk/core/core-auth/src/tokenCredential.ts) */ - getIdentityCredentialAsync(showDialog = true): Promise { + getIdentityCredentialAsync( + showDialog = true, + authenticationSessionRequest?: AuthenticationWWWAuthenticateRequest + ): Promise { + if (authenticationSessionRequest && authenticationSessionRequest.wwwAuthenticate) { + throw new MFARequiredError(cliSource); + } return Promise.resolve(AzureAccountManager.teamsFxTokenCredential); } @@ -557,6 +563,7 @@ async function listAll( import AzureLoginCI from "./azureLoginCI"; import AzureAccountProviderUserPassword from "./azureLoginUserPassword"; +import { cliSource } from "../constants"; // todo delete ciEnabled const azureLogin = !ui.interactive diff --git a/packages/fx-core/package.json b/packages/fx-core/package.json index f5fa9928d14..f39048c1c75 100644 --- a/packages/fx-core/package.json +++ b/packages/fx-core/package.json @@ -101,6 +101,7 @@ "@azure/arm-storage": "^17.2.1", "@azure/arm-subscriptions": "^5.0.0", "@azure/core-auth": "^1.4.0", + "@azure/core-rest-pipeline": "^1.22.1", "@azure/identity": "^4.1.0", "@azure/msal-node": "^2.6.6", "@azure/storage-blob": "^12.25.0", diff --git a/packages/fx-core/pnpm-lock.yaml b/packages/fx-core/pnpm-lock.yaml index 22d4bb45cfe..17b5fcc2d68 100644 --- a/packages/fx-core/pnpm-lock.yaml +++ b/packages/fx-core/pnpm-lock.yaml @@ -23,6 +23,9 @@ dependencies: '@azure/core-auth': specifier: ^1.4.0 version: 1.4.0 + '@azure/core-rest-pipeline': + specifier: ^1.22.1 + version: 1.22.1 '@azure/identity': specifier: ^4.1.0 version: 4.1.0 @@ -454,7 +457,7 @@ packages: '@azure/core-client': 1.9.4 '@azure/core-lro': 2.7.2 '@azure/core-paging': 1.6.2 - '@azure/core-rest-pipeline': 1.20.0 + '@azure/core-rest-pipeline': 1.22.1 tslib: 2.3.1 transitivePeerDependencies: - supports-color @@ -469,7 +472,7 @@ packages: '@azure/core-client': 1.9.4 '@azure/core-lro': 2.7.2 '@azure/core-paging': 1.6.2 - '@azure/core-rest-pipeline': 1.20.0 + '@azure/core-rest-pipeline': 1.22.1 tslib: 2.3.1 transitivePeerDependencies: - supports-color @@ -484,7 +487,7 @@ packages: '@azure/core-client': 1.9.4 '@azure/core-lro': 2.7.2 '@azure/core-paging': 1.6.2 - '@azure/core-rest-pipeline': 1.20.0 + '@azure/core-rest-pipeline': 1.22.1 tslib: 2.3.1 transitivePeerDependencies: - supports-color @@ -499,12 +502,23 @@ packages: '@azure/core-client': 1.9.4 '@azure/core-lro': 2.7.2 '@azure/core-paging': 1.6.2 - '@azure/core-rest-pipeline': 1.20.0 + '@azure/core-rest-pipeline': 1.22.1 tslib: 2.3.1 transitivePeerDependencies: - supports-color dev: false + /@azure/core-auth@1.10.1: + resolution: {integrity: sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==} + engines: {node: '>=20.0.0'} + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-util': 1.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + dev: false + /@azure/core-auth@1.4.0: resolution: {integrity: sha512-HFrcTgmuSuukRf/EdPmqBrc5l6Q5Uu+2TbuhaKbgaCpP2TfAeiNaQPAadxO+CYBRHGUzIDteMAjFspFLDLnKVQ==} engines: {node: '>=12.0.0'} @@ -530,7 +544,7 @@ packages: dependencies: '@azure/abort-controller': 2.1.2 '@azure/core-auth': 1.4.0 - '@azure/core-rest-pipeline': 1.20.0 + '@azure/core-rest-pipeline': 1.22.1 '@azure/core-tracing': 1.2.0 '@azure/core-util': 1.12.0 '@azure/logger': 1.2.0 @@ -545,7 +559,7 @@ packages: dependencies: '@azure/abort-controller': 2.1.2 '@azure/core-client': 1.9.4 - '@azure/core-rest-pipeline': 1.20.0 + '@azure/core-rest-pipeline': 1.22.1 transitivePeerDependencies: - supports-color dev: false @@ -569,16 +583,16 @@ packages: tslib: 2.8.1 dev: false - /@azure/core-rest-pipeline@1.20.0: - resolution: {integrity: sha512-ASoP8uqZBS3H/8N8at/XwFr6vYrRP3syTK0EUjDXQy0Y1/AUS+QeIRThKmTNJO2RggvBBxaXDPM7YoIwDGeA0g==} - engines: {node: '>=18.0.0'} + /@azure/core-rest-pipeline@1.22.1: + resolution: {integrity: sha512-UVZlVLfLyz6g3Hy7GNDpooMQonUygH7ghdiSASOOHy97fKj/mPLqgDX7aidOijn+sCMU+WU8NjlPlNTgnvbcGA==} + engines: {node: '>=20.0.0'} dependencies: '@azure/abort-controller': 2.1.2 - '@azure/core-auth': 1.9.0 - '@azure/core-tracing': 1.2.0 - '@azure/core-util': 1.12.0 - '@azure/logger': 1.2.0 - '@typespec/ts-http-runtime': 0.2.2 + '@azure/core-auth': 1.10.1 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + '@typespec/ts-http-runtime': 0.3.1 tslib: 2.8.1 transitivePeerDependencies: - supports-color @@ -591,6 +605,13 @@ packages: tslib: 2.8.1 dev: false + /@azure/core-tracing@1.3.1: + resolution: {integrity: sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==} + engines: {node: '>=20.0.0'} + dependencies: + tslib: 2.8.1 + dev: false + /@azure/core-util@1.12.0: resolution: {integrity: sha512-13IyjTQgABPARvG90+N2dXpC+hwp466XCdQXPCRlbWHgd3SJd5Q1VvaBGv6k1BIa4MQm6hAF1UBU1m8QUxV8sQ==} engines: {node: '>=18.0.0'} @@ -602,6 +623,17 @@ packages: - supports-color dev: false + /@azure/core-util@1.13.1: + resolution: {integrity: sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==} + engines: {node: '>=20.0.0'} + dependencies: + '@azure/abort-controller': 2.1.2 + '@typespec/ts-http-runtime': 0.3.1 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + dev: false + /@azure/core-xml@1.4.5: resolution: {integrity: sha512-gT4H8mTaSXRz7eGTuQyq1aIJnJqeXzpOe9Ay7Z3FrCouer14CbV3VzjnJrNrQfbBpGBLO9oy8BmrY75A0p53cA==} engines: {node: '>=18.0.0'} @@ -617,7 +649,7 @@ packages: '@azure/abort-controller': 1.1.0 '@azure/core-auth': 1.9.0 '@azure/core-client': 1.9.4 - '@azure/core-rest-pipeline': 1.20.0 + '@azure/core-rest-pipeline': 1.22.1 '@azure/core-tracing': 1.2.0 '@azure/core-util': 1.12.0 '@azure/logger': 1.2.0 @@ -642,6 +674,16 @@ packages: - supports-color dev: false + /@azure/logger@1.3.0: + resolution: {integrity: sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==} + engines: {node: '>=20.0.0'} + dependencies: + '@typespec/ts-http-runtime': 0.3.1 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + dev: false + /@azure/msal-browser@3.28.1: resolution: {integrity: sha512-OHHEWMB5+Zrix8yKvLVzU3rKDFvh7SOzAzXfICD7YgUXLxfHpTPX2pzOotrri1kskwhHqIj4a5LvhZlIqE7C7g==} engines: {node: '>=0.8.0'} @@ -678,7 +720,7 @@ packages: '@azure/core-http-compat': 2.3.0 '@azure/core-lro': 2.7.2 '@azure/core-paging': 1.6.2 - '@azure/core-rest-pipeline': 1.20.0 + '@azure/core-rest-pipeline': 1.22.1 '@azure/core-tracing': 1.2.0 '@azure/core-util': 1.12.0 '@azure/core-xml': 1.4.5 @@ -1490,6 +1532,17 @@ packages: - supports-color dev: false + /@typespec/ts-http-runtime@0.3.1: + resolution: {integrity: sha512-SnbaqayTVFEA6/tYumdF0UmybY0KHyKwGPBXnyckFlrrKdhWFrL3a2HIPXHjht5ZOElKGcXfD2D63P36btb+ww==} + engines: {node: '>=20.0.0'} + dependencies: + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + dev: false + /@webassemblyjs/ast@1.11.1: resolution: {integrity: sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==} dependencies: @@ -5614,7 +5667,7 @@ packages: /rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} dependencies: - tslib: 2.3.1 + tslib: 2.8.1 dev: true /safe-array-concat@1.1.3: @@ -6296,7 +6349,6 @@ packages: /tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - dev: false /tsutils@3.21.0(typescript@4.7.4): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} diff --git a/packages/fx-core/resource/package.nls.json b/packages/fx-core/resource/package.nls.json index 1cd8dc2d5cc..4a882ce961a 100644 --- a/packages/fx-core/resource/package.nls.json +++ b/packages/fx-core/resource/package.nls.json @@ -984,6 +984,7 @@ "error.common.ConcurrentError": "Previous task is still running. Wait until your previous task is finished and try again.", "error.common.NetworkError": "Network error: %s", "error.common.NetworkError.EAI_AGAIN": "DNS cannot resolve domain %s.", + "error.MFARequired": "Mandatory MFA is enforced by Azure policy - 'User must authenticate with multi-factor authentication to create or update resources'. Please enable MFA for your account to continue.", "error.upgrade.NoNeedUpgrade": "This is the latest project, upgrade not required.", "error.collaboration.InvalidManifestError": "Unable to process your manifest file ('%s') due to absence of the 'id' key. To identify your app correctly, make sure the 'id' key is present in the manifest file.", "error.collaboration.FailedToLoadManifest": "Unable to load manifest file. Reason: %s.", diff --git a/packages/fx-core/src/component/driver/arm/deployImpl.ts b/packages/fx-core/src/component/driver/arm/deployImpl.ts index 9a4c196b1b4..0b4ec65fd5f 100644 --- a/packages/fx-core/src/component/driver/arm/deployImpl.ts +++ b/packages/fx-core/src/component/driver/arm/deployImpl.ts @@ -27,6 +27,7 @@ import { convertOutputs, getFileExtension, hasBicepTemplate } from "./util/util" import { validateArgs } from "./validator"; import { ErrorContextMW } from "../../../common/globalVars"; import { hooks } from "@feathersjs/hooks"; +import { azureClientHelper } from "../../utils/azureClient"; const helpLink = "https://aka.ms/teamsfx-actions/arm-deploy"; @@ -43,7 +44,10 @@ export class ArmDeployImpl { public async run(): Promise> { await this.validateArgs(); - await this.createClient(); + this.client = await azureClientHelper.createRmClient( + this.context.azureAccountProvider, + this.args.subscriptionId + ); const needBicepCli = hasBicepTemplate(this.args.templates); if (needBicepCli && this.args.bicepCliVersion) { @@ -75,19 +79,6 @@ export class ArmDeployImpl { return await ensureBicepForDriver(this.context, this.args.bicepCliVersion!); } - private async createClient(): Promise { - this.context.logProvider.debug( - `Get token from AzureAccountProvider to create ResourceManagementClient of @azure/arm-resources` - ); - const azureToken = await this.context.azureAccountProvider.getIdentityCredentialAsync(); - if (!azureToken) { - throw new InvalidAzureCredentialError(); - } - this.client = new ResourceManagementClient(azureToken, this.args.subscriptionId, { - userAgentOptions: { userAgentPrefix: "TeamsToolkit" }, - }); - } - async deployTemplates(): Promise> { const outputs: deploymentOutput[] = []; this.setTelemetries(); diff --git a/packages/fx-core/src/component/utils/ResourceGroupHelper.ts b/packages/fx-core/src/component/utils/ResourceGroupHelper.ts index 5b53a1503dd..80ce205c05b 100644 --- a/packages/fx-core/src/component/utils/ResourceGroupHelper.ts +++ b/packages/fx-core/src/component/utils/ResourceGroupHelper.ts @@ -28,9 +28,9 @@ import { } from "../../error/azure"; import { QuestionNames, recommendedLocations } from "../../question/constants"; import { traverse } from "../../ui/visitor"; -import { SolutionSource } from "../constants"; import { getLocalizedString } from "../../common/localizeUtils"; import { InputValidationError } from "../../error"; +import { azureClientHelper } from "./azureClient"; const MsResources = "Microsoft.Resources"; const ResourceGroups = "resourceGroups"; @@ -56,10 +56,7 @@ export function selectResourceGroupQuestion( title: getLocalizedString("core.QuestionSelectResourceGroup.title"), staticOptions: [{ id: newResourceGroupOption, label: newResourceGroupOption }], dynamicOptions: async (inputs: Inputs): Promise => { - const rmClient = await resourceGroupHelper.createRmClient( - azureAccountProvider, - subscriptionId - ); + const rmClient = await azureClientHelper.createRmClient(azureAccountProvider, subscriptionId); const listRgRes = await resourceGroupHelper.listResourceGroups(rmClient); if (listRgRes.isErr()) throw listRgRes.error; const rgList = listRgRes.value; @@ -90,10 +87,7 @@ export function selectResourceGroupLocationQuestion( title: getLocalizedString("core.QuestionNewResourceGroupLocation.title"), staticOptions: [], dynamicOptions: async (inputs: Inputs) => { - const rmClient = await resourceGroupHelper.createRmClient( - azureAccountProvider, - subscriptionId - ); + const rmClient = await azureClientHelper.createRmClient(azureAccountProvider, subscriptionId); const getLocationsRes = await resourceGroupHelper.getLocations( azureAccountProvider, rmClient @@ -197,7 +191,7 @@ class ResourceGroupHelper { subscriptionId: string, location: string ): Promise> { - const rmClient = await this.createRmClient(azureAccountProvider, subscriptionId); + const rmClient = await azureClientHelper.createRmClient(azureAccountProvider, subscriptionId); const maybeExist = await this.checkResourceGroupExistence(resourceGroupName, rmClient); if (maybeExist.isErr()) { return err(maybeExist.error); @@ -391,16 +385,6 @@ class ResourceGroupHelper { }); } } - - async createRmClient(azureAccountProvider: AzureAccountProvider, subscriptionId: string) { - const azureToken = await azureAccountProvider.getIdentityCredentialAsync(); - if (azureToken === undefined) { - throw new InvalidAzureCredentialError(); - } - await azureAccountProvider.setSubscription(subscriptionId); - const rmClient = new ResourceManagementClient(azureToken, subscriptionId); - return rmClient; - } } export const resourceGroupHelper = new ResourceGroupHelper(); diff --git a/packages/fx-core/src/component/utils/azureClient.ts b/packages/fx-core/src/component/utils/azureClient.ts new file mode 100644 index 00000000000..d024097e1be --- /dev/null +++ b/packages/fx-core/src/component/utils/azureClient.ts @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { AuthenticationWWWAuthenticateRequest, AzureAccountProvider } from "@microsoft/teamsfx-api"; +import { ResourceManagementClient } from "@azure/arm-resources"; +import { InvalidAzureCredentialError } from "../../error"; +import { Pipeline, PipelinePolicy } from "@azure/core-rest-pipeline"; +import { BearerChallengePolicy } from "./pipelinePolicy"; +import { AzureScopes } from "../../common/constants"; + +class AzureClientHelper { + async createRmClient(azureAccountProvider: AzureAccountProvider, subscriptionId: string) { + const azureToken = await azureAccountProvider.getIdentityCredentialAsync(); + if (azureToken === undefined) { + throw new InvalidAzureCredentialError(); + } + await azureAccountProvider.setSubscription(subscriptionId); + const rmClient = new ResourceManagementClient(azureToken, subscriptionId, { + userAgentOptions: { userAgentPrefix: "AgentsToolkit" }, + }); + this.addPipelinePolicy( + rmClient.pipeline, + new BearerChallengePolicy(this.getChallengeHandler(azureAccountProvider)) + ); + return rmClient; + } + + addPipelinePolicy(pipeline: Pipeline, policy: PipelinePolicy) { + pipeline.addPolicy(policy, { phase: "Sign" }); + } + + getChallengeHandler = (tokenProvider: AzureAccountProvider) => { + const getTokenForChallenge = async ( + scopes: AuthenticationWWWAuthenticateRequest + ): Promise => { + const azureToken = await tokenProvider.getIdentityCredentialAsync(false, scopes); + if (!azureToken) { + throw new InvalidAzureCredentialError(); + } + const token = (await azureToken.getToken(AzureScopes)) as { token: string }; + return token.token; + }; + + return getTokenForChallenge; + }; +} + +export const azureClientHelper = new AzureClientHelper(); diff --git a/packages/fx-core/src/component/utils/pipelinePolicy.ts b/packages/fx-core/src/component/utils/pipelinePolicy.ts new file mode 100644 index 00000000000..0057449b3ea --- /dev/null +++ b/packages/fx-core/src/component/utils/pipelinePolicy.ts @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { + PipelinePolicy, + PipelineRequest, + SendRequest, + PipelineResponse, +} from "@azure/core-rest-pipeline"; +import { AuthenticationWWWAuthenticateRequest } from "@microsoft/teamsfx-api"; +import { AzureScopes } from "../../common/constants"; + +export class BearerChallengePolicy implements PipelinePolicy { + public readonly name = "BearerChallengePolicy"; + private readonly challengeRetryHeader = "x-atk-challenge-retry"; + + public constructor( + private readonly getTokenForChallenge: ( + scopes: AuthenticationWWWAuthenticateRequest + ) => Promise + ) {} + + public async sendRequest(request: PipelineRequest, next: SendRequest): Promise { + const initial = await next(request); + + // Only attempt a single retry on auth challenges + if (initial.status === 401 && !request.headers.get(this.challengeRetryHeader)) { + const header = + initial.headers.get("WWW-Authenticate") || initial.headers.get("www-authenticate"); + if (header) { + request.headers.set(this.challengeRetryHeader, "1"); + + const token = await this.getTokenForChallenge({ + wwwAuthenticate: header, + scopes: AzureScopes, + }); + if (token) { + request.headers.set("Authorization", `Bearer ${token}`); + return await next(request); + } + } + } + + return initial; + } +} diff --git a/packages/fx-core/src/error/common.ts b/packages/fx-core/src/error/common.ts index ae909e5f795..e08be96bd79 100644 --- a/packages/fx-core/src/error/common.ts +++ b/packages/fx-core/src/error/common.ts @@ -491,6 +491,17 @@ export class NotImplementedError extends SystemError { }); } } + +export class MFARequiredError extends UserError { + constructor(source: string) { + super({ + source: source, + message: getLocalizedString("error.MFARequired"), + displayMessage: getDefaultString("error.MFARequired"), + }); + } +} + export class ConcurrentError extends UserError { constructor(source: string) { super({ diff --git a/packages/fx-core/tests/component/util/azureClient.test.ts b/packages/fx-core/tests/component/util/azureClient.test.ts new file mode 100644 index 00000000000..abe17ef9740 --- /dev/null +++ b/packages/fx-core/tests/component/util/azureClient.test.ts @@ -0,0 +1,50 @@ +import chai from "chai"; +import chaiAsPromised from "chai-as-promised"; +import "mocha"; +import * as sinon from "sinon"; +import { MockedAzureAccountProvider } from "../../core/utils"; +import { azureClientHelper } from "../../../src/component/utils/azureClient"; +import { InvalidAzureCredentialError } from "../../../src/error"; + +chai.use(chaiAsPromised); + +describe("azureClient test", () => { + const sandbox = sinon.createSandbox(); + + beforeEach(async () => {}); + + afterEach(async () => { + sandbox.restore(); + }); + + it("getChallengeHandler returns invalid token", async () => { + const tokenProvider = new MockedAzureAccountProvider(); + sandbox.stub(tokenProvider, "getIdentityCredentialAsync").resolves(undefined); + + const getTokenForChallenge = azureClientHelper.getChallengeHandler(tokenProvider); + try { + getTokenForChallenge({ + wwwAuthenticate: "faked-claim", + scopes: ["https://management.azure.com/.default"], + }); + } catch (e) { + chai.assert.isTrue(e instanceof InvalidAzureCredentialError); + } + }); + + it("getChallengeHandler happy pass", async () => { + const tokenProvider = new MockedAzureAccountProvider(); + sandbox.stub(tokenProvider, "getIdentityCredentialAsync").resolves({ + getToken: async function (scopes: string | string[]) { + return { token: "fake-token", expiresOnTimestamp: 0 }; + }, + }); + + const getTokenForChallenge = azureClientHelper.getChallengeHandler(tokenProvider); + const token = await getTokenForChallenge({ + wwwAuthenticate: "faked-claim", + scopes: ["https://management.azure.com/.default"], + }); + chai.assert.equal(token, "fake-token"); + }); +}); diff --git a/packages/fx-core/tests/component/util/pipelinePolicy.test.ts b/packages/fx-core/tests/component/util/pipelinePolicy.test.ts new file mode 100644 index 00000000000..8fbe2e5f26a --- /dev/null +++ b/packages/fx-core/tests/component/util/pipelinePolicy.test.ts @@ -0,0 +1,81 @@ +import chai from "chai"; +import chaiAsPromised from "chai-as-promised"; +import "mocha"; +import * as sinon from "sinon"; +import { MockedAzureAccountProvider } from "../../core/utils"; +import { azureClientHelper } from "../../../src/component/utils/azureClient"; +import { InvalidAzureCredentialError } from "../../../src/error"; +import { BearerChallengePolicy } from "../../../src/component/utils/pipelinePolicy"; +import { get, head } from "lodash"; + +chai.use(chaiAsPromised); + +describe("BearerChallengePolicy test", () => { + const sandbox = sinon.createSandbox(); + + beforeEach(async () => {}); + + afterEach(async () => { + sandbox.restore(); + }); + + it("returns 401 error and trigger challenge retrieval", async () => { + const challengePolicy = new BearerChallengePolicy(async (scopes) => { + return "fake-token"; + }); + + const next = async (request: any) => { + return { + status: 401, + headers: { + get: (key: string) => { + return key === "WWW-Authenticate" ? "faked-claim" : undefined; + }, + }, + }; + }; + const request = { + headers: { + map: new Map(), + get(key: string) { + return this.map.get(key); + }, + set(key: string, value: string) { + this.map.set(key, value); + }, + }, + }; + const response = await challengePolicy.sendRequest(request as any, next as any); + chai.assert.equal(response.status, 401); + }); + + it("returns 200", async () => { + const challengePolicy = new BearerChallengePolicy(async (scopes) => { + return "fake-token"; + }); + + const next = async (request: any) => { + return { + status: 200, + headers: { + get: (key: string) => { + return key === "Authorization" ? "Bearer fake-token" : undefined; + }, + }, + }; + }; + const request = { + headers: { + map: new Map(), + get(key: string) { + return this.map.get(key); + }, + set(key: string, value: string) { + this.map.set(key, value); + }, + }, + }; + const response = await challengePolicy.sendRequest(request as any, next as any); + chai.assert.equal(response.status, 200); + }); +}); diff --git a/packages/fx-core/tests/question/question.test.ts b/packages/fx-core/tests/question/question.test.ts index 58a8ece7da0..18b55592eb7 100644 --- a/packages/fx-core/tests/question/question.test.ts +++ b/packages/fx-core/tests/question/question.test.ts @@ -64,6 +64,7 @@ import { } from "../../src/question/other"; import { QuestionTreeVisitor, traverse } from "../../src/ui/visitor"; import { MockTools, MockUserInteraction, MockedAzureAccountProvider } from "../core/utils"; +import { azureClientHelper } from "../../src/component/utils/azureClient"; const ui = new MockUserInteraction(); export async function callFuncs(question: Question, inputs: Inputs, answer?: string) { @@ -697,7 +698,7 @@ describe("resourceGroupQuestionNode", async () => { mockToken as TokenCredential, mockSubscriptionId ); - sandbox.stub(resourceGroupHelper, "createRmClient").resolves(mockRmClient); + sandbox.stub(azureClientHelper, "createRmClient").resolves(mockRmClient); const node = resourceGroupQuestionNode(accountProvider, mockSubscriptionId, defaultRG); assert.isTrue(node !== undefined); const inputs: Inputs = { @@ -750,7 +751,7 @@ describe("resourceGroupQuestionNode", async () => { mockToken as TokenCredential, mockSubscriptionId ); - sandbox.stub(resourceGroupHelper, "createRmClient").resolves(mockRmClient); + sandbox.stub(azureClientHelper, "createRmClient").resolves(mockRmClient); const node = resourceGroupQuestionNode(accountProvider, mockSubscriptionId, defaultRG); assert.isTrue(node !== undefined); const inputs: Inputs = { diff --git a/packages/server/src/providers/token/azure.ts b/packages/server/src/providers/token/azure.ts index 261400fafe1..ca34b2ee2ef 100644 --- a/packages/server/src/providers/token/azure.ts +++ b/packages/server/src/providers/token/azure.ts @@ -5,6 +5,7 @@ import { MessageConnection } from "vscode-jsonrpc"; import { TokenCredential } from "@azure/core-auth"; import { + AuthenticationWWWAuthenticateRequest, AzureAccountProvider, FxError, Result, @@ -15,7 +16,7 @@ import { import { RequestTypes } from "../../apis"; import { getResponseWithErrorHandling } from "../../utils"; import { AccessToken, GetTokenOptions } from "@azure/identity"; -import { NotImplementedError } from "@microsoft/teamsfx-core"; +import { MFARequiredError, NotImplementedError } from "@microsoft/teamsfx-core"; class TeamsFxTokenCredential implements TokenCredential { private connection: MessageConnection; @@ -64,7 +65,13 @@ export default class ServerAzureAccountProvider implements AzureAccountProvider this.teamsFxTokenCredential = new TeamsFxTokenCredential(this.connection); } - async getIdentityCredentialAsync(showDialog?: boolean): Promise { + async getIdentityCredentialAsync( + showDialog?: boolean, + authenticationSessionRequest?: AuthenticationWWWAuthenticateRequest + ): Promise { + if (authenticationSessionRequest && authenticationSessionRequest.wwwAuthenticate) { + throw new MFARequiredError("FxServer"); + } return Promise.resolve(this.teamsFxTokenCredential); } diff --git a/packages/server/tests/providers/token/azure.test.ts b/packages/server/tests/providers/token/azure.test.ts index c80453c5c5b..b6e55dd8466 100644 --- a/packages/server/tests/providers/token/azure.test.ts +++ b/packages/server/tests/providers/token/azure.test.ts @@ -72,6 +72,19 @@ describe("azure", () => { chai.assert.isNull(res); }); + it("getIdentityCredentialAsync returns MFA required error", async () => { + const azure = new ServerAzureAccountProvider(msgConn); + + try { + await azure.getIdentityCredentialAsync(false, { + wwwAuthenticate: "test", + scopes: [], + }); + } catch (e) { + chai.assert.equal((e as any).name, "MFARequiredError"); + } + }); + it("signout", async () => { const azure = new ServerAzureAccountProvider(msgConn); chai.expect(() => azure.signout()).to.throw(NotImplementedError); diff --git a/packages/vscode-extension/package.json b/packages/vscode-extension/package.json index 79b51895909..7a25af23dae 100644 --- a/packages/vscode-extension/package.json +++ b/packages/vscode-extension/package.json @@ -8,6 +8,10 @@ "private": true, "icon": "media/m365-agents-toolkit-color.png", "main": "./out/src/extension.js", + "enabledApiProposals": [ + "authenticationChallenges", + "authLearnMore" + ], "repository": { "type": "git", "url": "https://github.com/OfficeDev/TeamsFx" @@ -21,7 +25,7 @@ ] }, "engines": { - "vscode": "^1.93.0" + "vscode": "^1.105.0" }, "license": "MIT", "keywords": [ @@ -1955,7 +1959,7 @@ "@types/tmp": "^0.2.0", "@types/uuid": "^8.3.0", "@types/validator": "^13.1.1", - "@types/vscode": "^1.93.0", + "@types/vscode": "^1.104.0", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", "@vitejs/plugin-react": "^4.3.1", diff --git a/packages/vscode-extension/pnpm-lock.yaml b/packages/vscode-extension/pnpm-lock.yaml index 66465cebff5..d195390ad59 100644 --- a/packages/vscode-extension/pnpm-lock.yaml +++ b/packages/vscode-extension/pnpm-lock.yaml @@ -221,8 +221,8 @@ devDependencies: specifier: ^13.1.1 version: 13.1.1 '@types/vscode': - specifier: ^1.93.0 - version: 1.93.0 + specifier: ^1.104.0 + version: 1.104.0 '@typescript-eslint/eslint-plugin': specifier: ^5.0.0 version: 5.0.0(@typescript-eslint/parser@5.0.0)(eslint@8.1.0)(typescript@4.7.4) @@ -3414,8 +3414,8 @@ packages: resolution: {integrity: sha512-/39uXOKe1KV4ElXb8cp0RVyeRn9X1QRwllComMMql+FQ9nhmTx0Yhw+kJtccPSShsjma+KXjW/TiXyGUNqNn+w==} dev: true - /@types/vscode@1.93.0: - resolution: {integrity: sha512-kUK6jAHSR5zY8ps42xuW89NLcBpw1kOabah7yv38J8MyiYuOHxLQBi0e7zeXbQgVefDy/mZZetqEFC+Fl5eIEQ==} + /@types/vscode@1.104.0: + resolution: {integrity: sha512-0KwoU2rZ2ecsTGFxo4K1+f+AErRsYW0fsp6A0zufzGuhyczc2IoKqYqcwXidKXmy2u8YB2GsYsOtiI9Izx3Tig==} dev: true /@types/which@2.0.2: diff --git a/packages/vscode-extension/src/commonlib/azureLogin.ts b/packages/vscode-extension/src/commonlib/azureLogin.ts index 3eaecd49bcf..3001d41ab51 100644 --- a/packages/vscode-extension/src/commonlib/azureLogin.ts +++ b/packages/vscode-extension/src/commonlib/azureLogin.ts @@ -82,10 +82,13 @@ export class AzureAccountManager extends login implements AzureAccountProvider { /** * Async get identity [crendential](https://github.com/Azure/azure-sdk-for-js/blob/master/sdk/core/core-auth/src/tokenCredential.ts) */ - async getIdentityCredentialAsync(showDialog = true): Promise { + async getIdentityCredentialAsync( + showDialog = true, + authenticationSessionRequest?: vscode.AuthenticationWwwAuthenticateRequest + ): Promise { const tenantId = await loadTenantId(azureCacheName); if (await this.isUserLogin(tenantId)) { - const res = await this.getIdentityCredentialSilently(tenantId); + const res = await this.getIdentityCredentialSilently(tenantId, authenticationSessionRequest); if (res.isOk()) { return res.value; } else { @@ -177,7 +180,9 @@ export class AzureAccountManager extends login implements AzureAccountProvider { } } - private async doGetIdentityCredentialAsync(): Promise { + private async doGetIdentityCredentialAsync( + showDialog?: boolean + ): Promise { const tokenCredential = await this.doGetAccountCredentialAsync(); if (tokenCredential) { return tokenCredential; @@ -220,12 +225,19 @@ export class AzureAccountManager extends login implements AzureAccountProvider { } private async getIdentityCredentialSilently( - tenantId?: string + tenantId?: string, + authenticationSessionRequest?: vscode.AuthenticationWwwAuthenticateRequest ): Promise> { - const session = await getSessionFromVSCode(AzureScopes, tenantId, { - createIfNone: false, - silent: true, - }); + const session = await getSessionFromVSCode( + authenticationSessionRequest ?? AzureScopes, + tenantId, + authenticationSessionRequest + ? { createIfNone: true, silent: false } + : { + createIfNone: false, + silent: true, + } + ); if (!session) { return err(LoginFailureError()); } diff --git a/packages/vscode-extension/src/commonlib/vscodeAzureSubscriptionProvider.ts b/packages/vscode-extension/src/commonlib/vscodeAzureSubscriptionProvider.ts index bf36ebb67f9..e41ab6e2dcb 100644 --- a/packages/vscode-extension/src/commonlib/vscodeAzureSubscriptionProvider.ts +++ b/packages/vscode-extension/src/commonlib/vscodeAzureSubscriptionProvider.ts @@ -130,11 +130,15 @@ export class VSCodeAzureSubscriptionProvider { } export async function getSessionFromVSCode( - scopes?: string | string[], + scopes?: string | string[] | vscode.AuthenticationWwwAuthenticateRequest, tenantId?: string, options?: vscode.AuthenticationGetSessionOptions ): Promise { - return await vscode.authentication.getSession(Microsoft, getScopes(scopes, tenantId), options); + return await vscode.authentication.getSession( + Microsoft, + formScopesArg(scopes, tenantId), + options + ); } function ensureEndingSlash(value: string): string { @@ -162,6 +166,28 @@ function getScopes(scopes: string | string[] | undefined, tenantId?: string): st return scopeArr; } +export function isAuthenticationSessionRequest( + scopes?: string | string[] | vscode.AuthenticationWwwAuthenticateRequest +): scopes is vscode.AuthenticationWwwAuthenticateRequest { + return !!(scopes && typeof scopes === "object" && "wwwAuthenticate" in scopes); +} + +function formScopesArg( + scopes?: string | string[] | vscode.AuthenticationWwwAuthenticateRequest, + tenantId?: string +): string[] | vscode.AuthenticationWwwAuthenticateRequest { + const initialScopeList: string[] | undefined = + typeof scopes === "string" + ? [scopes] + : Array.isArray(scopes) + ? scopes + : Array.from(scopes?.scopes ?? []); + const scopeList = getScopes(initialScopeList, tenantId); + return isAuthenticationSessionRequest(scopes) + ? { fallbackScopes: scopeList, wwwAuthenticate: scopes.wwwAuthenticate } + : scopeList; +} + /** * Represents a means of obtaining authentication data for an Azure subscription. */ diff --git a/packages/vscode-extension/src/debug/officeTaskHandler.ts b/packages/vscode-extension/src/debug/officeTaskHandler.ts index a25e2abc8fb..9cd779d7ffe 100644 --- a/packages/vscode-extension/src/debug/officeTaskHandler.ts +++ b/packages/vscode-extension/src/debug/officeTaskHandler.ts @@ -59,13 +59,15 @@ function isDebugPreLaunchTask(task: vscode.Task): boolean { // Debug: Excel Desktop if (task.execution && task.execution) { const execution = task.execution; - const commandLine = - execution.commandLine || - `${typeof execution.command === "string" ? execution.command : execution.command.value} ${( - execution.args || [] - ).join(" ")}`; - if (/npm[\s]+run[\s]+start:desktop -- --app (word|excel|powerpoint)/i.test(commandLine)) { - return true; + if (execution.command) { + const commandLine = + execution.commandLine || + `${ + typeof execution.command === "string" ? execution.command : execution.command.value + } ${(execution.args || []).join(" ")}`; + if (/npm[\s]+run[\s]+start:desktop -- --app (word|excel|powerpoint)/i.test(commandLine)) { + return true; + } } } } diff --git a/packages/vscode-extension/src/debug/teamsfxTaskHandler.ts b/packages/vscode-extension/src/debug/teamsfxTaskHandler.ts index 5ac5c6aead8..3be930dbda4 100644 --- a/packages/vscode-extension/src/debug/teamsfxTaskHandler.ts +++ b/packages/vscode-extension/src/debug/teamsfxTaskHandler.ts @@ -139,11 +139,13 @@ function isTeamsfxTask(task: vscode.Task): boolean { let commandLine: string | undefined; if (task.execution && task.execution) { const execution = task.execution; - commandLine = - execution.commandLine || - `${typeof execution.command === "string" ? execution.command : execution.command.value} ${( - execution.args || [] - ).join(" ")}`; + if (execution.command) { + commandLine = + execution.commandLine || + `${ + typeof execution.command === "string" ? execution.command : execution.command.value + } ${(execution.args || []).join(" ")}`; + } } if (commandLine !== undefined) { if (/(npm|yarn)[\s]+(run )?[\s]*[^:\s]+:teamsfx/i.test(commandLine)) { @@ -166,13 +168,15 @@ function isLaunchTestToolTask(task: vscode.Task): boolean { // dev:teamsfx and watch:teamsfx if (task.execution && task.execution) { const execution = task.execution; - const commandLine = - execution.commandLine || - `${typeof execution.command === "string" ? execution.command : execution.command.value} ${( - execution.args || [] - ).join(" ")}`; - if (/(npm|yarn)[\s]+(run )?[\s]*[^:\s]+:teamsfx:launch-testtool/i.test(commandLine)) { - return true; + if (execution.command) { + const commandLine = + execution.commandLine || + `${ + typeof execution.command === "string" ? execution.command : execution.command.value + } ${(execution.args || []).join(" ")}`; + if (/(npm|yarn)[\s]+(run )?[\s]*[^:\s]+:teamsfx:launch-testtool/i.test(commandLine)) { + return true; + } } } } diff --git a/packages/vscode-extension/src/vscode.proposed.authLearnMore.d.ts b/packages/vscode-extension/src/vscode.proposed.authLearnMore.d.ts new file mode 100644 index 00000000000..8c3778285c3 --- /dev/null +++ b/packages/vscode-extension/src/vscode.proposed.authLearnMore.d.ts @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + + +declare module "vscode" { + + // https://github.com/microsoft/vscode/issues/206587 + + export interface AuthenticationGetSessionPresentationOptions { + /** + * An optional Uri to open in the browser to learn more about this authentication request. + */ + learnMore?: Uri; + } +} diff --git a/packages/vscode-extension/src/vscode.proposed.authenticationChallenges.d.ts b/packages/vscode-extension/src/vscode.proposed.authenticationChallenges.d.ts new file mode 100644 index 00000000000..0f5e8eb82d8 --- /dev/null +++ b/packages/vscode-extension/src/vscode.proposed.authenticationChallenges.d.ts @@ -0,0 +1,177 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + + +declare module "vscode" { + + // https://github.com/microsoft/vscode/issues/260156 + + /********** + * "Extension asking for auth" API + *******/ + + /** + * Represents parameters for creating a session based on a WWW-Authenticate header value. + * This is used when an API returns a 401 with a WWW-Authenticate header indicating + * that additional authentication is required. The details of which will be passed down + * to the authentication provider to create a session. + * + * @note The authorization provider must support handling challenges and specifically + * the challenges in this WWW-Authenticate value. + * @note For more information on WWW-Authenticate please see https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/WWW-Authenticate + */ + export interface AuthenticationWwwAuthenticateRequest { + /** + * The raw WWW-Authenticate header value that triggered this challenge. + * This will be parsed by the authentication provider to extract the necessary + * challenge information. + */ + readonly wwwAuthenticate: string; + + /** + * The fallback scopes to use if no scopes are found in the WWW-Authenticate header. + */ + readonly fallbackScopes?: readonly string[]; + + /** + * @deprecated Use `fallbackScopes` instead. + */ + readonly scopes?: readonly string[]; + } + + export namespace authentication { + /** + * Get an authentication session matching the desired scopes or satisfying the WWW-Authenticate request. Rejects if + * a provider with providerId is not registered, or if the user does not consent to sharing authentication information + * with the extension. If there are multiple sessions with the same scopes, the user will be shown a quickpick to + * select which account they would like to use. + * + * Built-in auth providers include: + * * 'github' - For GitHub.com + * * 'microsoft' For both personal & organizational Microsoft accounts + * * (less common) 'github-enterprise' - for alternative GitHub hostings, GHE.com, GitHub Enterprise Server + * * (less common) 'microsoft-sovereign-cloud' - for alternative Microsoft clouds + * + * @param providerId The id of the provider to use + * @param scopeListOrRequest A scope list of permissions requested or a WWW-Authenticate request. These are dependent on the authentication provider. + * @param options The {@link AuthenticationGetSessionOptions} to use + * @returns A thenable that resolves to an authentication session + */ + export function getSession(providerId: string, scopeListOrRequest: ReadonlyArray | AuthenticationWwwAuthenticateRequest, options: AuthenticationGetSessionOptions & { /** */createIfNone: true | AuthenticationGetSessionPresentationOptions }): Thenable; + + /** + * Get an authentication session matching the desired scopes or request. Rejects if a provider with providerId is not + * registered, or if the user does not consent to sharing authentication information with the extension. If there + * are multiple sessions with the same scopes, the user will be shown a quickpick to select which account they would like to use. + * + * Built-in auth providers include: + * * 'github' - For GitHub.com + * * 'microsoft' For both personal & organizational Microsoft accounts + * * (less common) 'github-enterprise' - for alternative GitHub hostings, GHE.com, GitHub Enterprise Server + * * (less common) 'microsoft-sovereign-cloud' - for alternative Microsoft clouds + * + * @param providerId The id of the provider to use + * @param scopeListOrRequest A scope list of permissions requested or a WWW-Authenticate request. These are dependent on the authentication provider. + * @param options The {@link AuthenticationGetSessionOptions} to use + * @returns A thenable that resolves to an authentication session + */ + export function getSession(providerId: string, scopeListOrRequest: ReadonlyArray | AuthenticationWwwAuthenticateRequest, options: AuthenticationGetSessionOptions & { /** literal-type defines return type */forceNewSession: true | AuthenticationGetSessionPresentationOptions | AuthenticationForceNewSessionOptions }): Thenable; + + /** + * Get an authentication session matching the desired scopes or request. Rejects if a provider with providerId is not + * registered, or if the user does not consent to sharing authentication information with the extension. If there + * are multiple sessions with the same scopes, the user will be shown a quickpick to select which account they would like to use. + * + * Built-in auth providers include: + * * 'github' - For GitHub.com + * * 'microsoft' For both personal & organizational Microsoft accounts + * * (less common) 'github-enterprise' - for alternative GitHub hostings, GHE.com, GitHub Enterprise Server + * * (less common) 'microsoft-sovereign-cloud' - for alternative Microsoft clouds + * + * @param providerId The id of the provider to use + * @param scopeListOrRequest A scope list of permissions requested or a WWW-Authenticate request. These are dependent on the authentication provider. + * @param options The {@link AuthenticationGetSessionOptions} to use + * @returns A thenable that resolves to an authentication session or undefined if a silent flow was used and no session was found + */ + export function getSession(providerId: string, scopeListOrRequest: ReadonlyArray | AuthenticationWwwAuthenticateRequest, options?: AuthenticationGetSessionOptions): Thenable; + } + + + /********** + * "Extension providing auth" API + * NOTE: This doesn't need to be finalized with the above + *******/ + + /** + * Represents an authentication challenge from a WWW-Authenticate header. + * This is used to handle cases where additional authentication steps are required, + * such as when mandatory multi-factor authentication (MFA) is enforced. + * + * @note For more information on WWW-Authenticate please see https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/WWW-Authenticate + */ + export interface AuthenticationChallenge { + /** + * The authentication scheme (e.g., 'Bearer'). + */ + readonly scheme: string; + + /** + * Parameters for the authentication challenge. + * For Bearer challenges, this may include 'claims', 'scope', 'realm', etc. + */ + readonly params: Record; + } + + /** + * Represents constraints for authentication, including challenges and optional scopes. + * This is used when creating or retrieving sessions that must satisfy specific authentication + * requirements from WWW-Authenticate headers. + * + * @note For more information on WWW-Authenticate please see https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/WWW-Authenticate + */ + export interface AuthenticationConstraint { + /** + * Array of authentication challenges parsed from WWW-Authenticate headers. + */ + readonly challenges: readonly AuthenticationChallenge[]; + + /** + * Optional scopes for the session. If not provided, the authentication provider + * may extract scopes from the challenges or use default scopes. + */ + readonly fallbackScopes?: readonly string[]; + } + + /** + * An authentication provider that supports challenge-based authentication. + * This extends the base AuthenticationProvider with methods to handle authentication + * challenges from WWW-Authenticate headers. + * + * TODO: Enforce that both of these functions should be defined by creating a new AuthenticationProviderWithChallenges interface. + * But this can be done later since this part doesn't need finalization. + */ + export interface AuthenticationProvider { + /** + * Get existing sessions that match the given authentication constraints. + * + * @param constraint The authentication constraint containing challenges and optional scopes + * @param options Options for the session request + * @returns A thenable that resolves to an array of existing authentication sessions + */ + getSessionsFromChallenges?(constraint: AuthenticationConstraint, options: AuthenticationProviderSessionOptions): Thenable; + + /** + * Create a new session based on authentication constraints. + * This is called when no existing session matches the constraint requirements. + * + * @param constraint The authentication constraint containing challenges and optional scopes + * @param options Options for the session creation + * @returns A thenable that resolves to a new authentication session + */ + createSessionFromChallenges?(constraint: AuthenticationConstraint, options: AuthenticationProviderSessionOptions): Thenable; + } + + export interface AuthenticationProviderOptions { + supportsChallenges?: boolean; + } +} diff --git a/packages/vscode-extension/test/chat/utils.test.ts b/packages/vscode-extension/test/chat/utils.test.ts.disable similarity index 100% rename from packages/vscode-extension/test/chat/utils.test.ts rename to packages/vscode-extension/test/chat/utils.test.ts.disable diff --git a/packages/vscode-extension/test/mocks/vsc/chat.ts b/packages/vscode-extension/test/mocks/vsc/chat.ts index aeb137423ce..45fb44842c7 100644 --- a/packages/vscode-extension/test/mocks/vsc/chat.ts +++ b/packages/vscode-extension/test/mocks/vsc/chat.ts @@ -27,7 +27,7 @@ export class LanguageModelChatMessage { /** * The content of this message. */ - content: string; + content: Array<{ value: string }>; /** * The optional name of a user for this message. @@ -43,7 +43,7 @@ export class LanguageModelChatMessage { */ constructor(role: LanguageModelChatMessageRole, content: string, name?: string) { this.role = role; - this.content = content; + this.content = [{ value: content }]; this.name = name; } } diff --git a/packages/vscode-extension/test/officeChat/common/planner.test.ts b/packages/vscode-extension/test/officeChat/common/planner.test.ts index 0d021e8aed4..95025906cdb 100644 --- a/packages/vscode-extension/test/officeChat/common/planner.test.ts +++ b/packages/vscode-extension/test/officeChat/common/planner.test.ts @@ -43,7 +43,7 @@ describe("planner", () => { invokeParametersInit = function () { const model: LanguageModelChatMessage = { role: LanguageModelChatMessageRole.User, - content: "", + content: [{ value: "" }], name: undefined, }; diff --git a/packages/vscode-extension/test/officeChat/common/samples/sampleProvider.test.ts b/packages/vscode-extension/test/officeChat/common/samples/sampleProvider.test.ts.disable similarity index 100% rename from packages/vscode-extension/test/officeChat/common/samples/sampleProvider.test.ts rename to packages/vscode-extension/test/officeChat/common/samples/sampleProvider.test.ts.disable diff --git a/packages/vscode-extension/test/officeChat/common/skills/codeExplainer.test.ts b/packages/vscode-extension/test/officeChat/common/skills/codeExplainer.test.ts index ee0e2f5902a..3cf1ceaeaea 100644 --- a/packages/vscode-extension/test/officeChat/common/skills/codeExplainer.test.ts +++ b/packages/vscode-extension/test/officeChat/common/skills/codeExplainer.test.ts @@ -52,7 +52,7 @@ describe("CodeExplainer", () => { const model: LanguageModelChatMessage = { role: LanguageModelChatMessageRole.User, - content: "", + content: [{ value: "" }], name: undefined, }; diff --git a/packages/vscode-extension/test/officeChat/common/skills/codeGenerator.test.ts b/packages/vscode-extension/test/officeChat/common/skills/codeGenerator.test.ts.disable similarity index 99% rename from packages/vscode-extension/test/officeChat/common/skills/codeGenerator.test.ts rename to packages/vscode-extension/test/officeChat/common/skills/codeGenerator.test.ts.disable index eb111c6b30f..f4f56dd5ec9 100644 --- a/packages/vscode-extension/test/officeChat/common/skills/codeGenerator.test.ts +++ b/packages/vscode-extension/test/officeChat/common/skills/codeGenerator.test.ts.disable @@ -53,7 +53,7 @@ describe("codeGenerator", () => { const model: LanguageModelChatMessage = { role: LanguageModelChatMessageRole.User, - content: "", + content: [{ value: "" }], name: undefined, }; diff --git a/packages/vscode-extension/test/officeChat/common/skills/codeIssueCorrector.test.ts b/packages/vscode-extension/test/officeChat/common/skills/codeIssueCorrector.test.ts index 9f7cf458b43..1bdd004a4ef 100644 --- a/packages/vscode-extension/test/officeChat/common/skills/codeIssueCorrector.test.ts +++ b/packages/vscode-extension/test/officeChat/common/skills/codeIssueCorrector.test.ts @@ -16,6 +16,7 @@ import { import { ExecutionResultEnum } from "../../../../src/officeChat/common/skills/executionResultEnum"; import { SampleProvider } from "../../../../src/officeChat/common/samples/sampleProvider"; import { SampleData } from "../../../../src/officeChat/common/samples/sampleData"; +import { values } from "lodash"; describe("CodeIssueCorrector", () => { const sandbox = sinon.createSandbox(); @@ -56,7 +57,7 @@ describe("CodeIssueCorrector", () => { const model: LanguageModelChatMessage = { role: LanguageModelChatMessageRole.User, - content: "", + content: [{ value: "" }], name: undefined, }; @@ -146,12 +147,12 @@ describe("CodeIssueCorrector", () => { const corrector = new CodeIssueCorrector(); const fakeLanguageModelChatSystemMessage: LanguageModelChatMessage = { role: LanguageModelChatMessageRole.User, - content: "some sample message", + content: [{ value: "some sample message" }], name: undefined, }; const fakeSampleCodeLanguageModelChatSystemMessage: LanguageModelChatMessage = { role: LanguageModelChatMessageRole.User, - content: sampleCodeLong, + content: [{ value: sampleCodeLong }], name: undefined, }; const spec = new Spec("some user input"); @@ -194,12 +195,12 @@ describe("CodeIssueCorrector", () => { const corrector = new CodeIssueCorrector(); const fakeLanguageModelChatSystemMessage: LanguageModelChatMessage = { role: LanguageModelChatMessageRole.User, - content: "some sample message", + content: [{ value: "some sample message" }], name: undefined, }; const fakeSampleCodeLanguageModelChatSystemMessage: LanguageModelChatMessage = { role: LanguageModelChatMessageRole.User, - content: sampleCodeLong, + content: [{ value: sampleCodeLong }], name: undefined, }; @@ -252,12 +253,12 @@ describe("CodeIssueCorrector", () => { const corrector = new CodeIssueCorrector(); const fakeLanguageModelChatSystemMessage: LanguageModelChatMessage = { role: LanguageModelChatMessageRole.User, - content: "some sample message", + content: [{ value: "some sample message" }], name: undefined, }; const fakeSampleCodeLanguageModelChatSystemMessage: LanguageModelChatMessage = { role: LanguageModelChatMessageRole.User, - content: sampleCodeLong, + content: [{ value: sampleCodeLong }], name: undefined, }; @@ -310,12 +311,12 @@ describe("CodeIssueCorrector", () => { const corrector = new CodeIssueCorrector(); const fakeLanguageModelChatSystemMessage: LanguageModelChatMessage = { role: LanguageModelChatMessageRole.User, - content: "some sample message", + content: [{ value: "some sample message" }], name: undefined, }; const fakeSampleCodeLanguageModelChatSystemMessage: LanguageModelChatMessage = { role: LanguageModelChatMessageRole.User, - content: sampleCodeLong, + content: [{ value: sampleCodeLong }], name: undefined, }; @@ -361,7 +362,7 @@ describe("CodeIssueCorrector", () => { const corrector = new CodeIssueCorrector(); const fakeLanguageModelChatSystemMessage: LanguageModelChatMessage = { role: LanguageModelChatMessageRole.User, - content: "some sample message", + content: [{ value: "some sample message" }], name: undefined, }; @@ -403,7 +404,7 @@ describe("CodeIssueCorrector", () => { const corrector = new CodeIssueCorrector(); const fakeLanguageModelChatSystemMessage: LanguageModelChatMessage = { role: LanguageModelChatMessageRole.User, - content: "some sample message", + content: [{ value: "some sample message" }], name: undefined, }; diff --git a/packages/vscode-extension/test/officeChat/common/skills/printer.test.ts b/packages/vscode-extension/test/officeChat/common/skills/printer.test.ts index 7bf358ebb4f..7fb2216b1d3 100644 --- a/packages/vscode-extension/test/officeChat/common/skills/printer.test.ts +++ b/packages/vscode-extension/test/officeChat/common/skills/printer.test.ts @@ -52,7 +52,7 @@ describe("printer", () => { const model: LanguageModelChatMessage = { role: LanguageModelChatMessageRole.User, - content: "", + content: [{ value: "" }], name: undefined, }; diff --git a/packages/vscode-extension/test/officeChat/common/skills/projectCreator.test.ts b/packages/vscode-extension/test/officeChat/common/skills/projectCreator.test.ts index b1f39476624..f289faa4241 100644 --- a/packages/vscode-extension/test/officeChat/common/skills/projectCreator.test.ts +++ b/packages/vscode-extension/test/officeChat/common/skills/projectCreator.test.ts @@ -52,7 +52,7 @@ describe("projectCreator", () => { const model: LanguageModelChatMessage = { role: LanguageModelChatMessageRole.User, - content: "", + content: [{ value: "" }], name: undefined, }; diff --git a/packages/vscode-extension/test/officeChat/common/skills/skillset.test.ts b/packages/vscode-extension/test/officeChat/common/skills/skillset.test.ts index dcf09df5827..92b8be9fea3 100644 --- a/packages/vscode-extension/test/officeChat/common/skills/skillset.test.ts +++ b/packages/vscode-extension/test/officeChat/common/skills/skillset.test.ts @@ -52,7 +52,7 @@ describe("skillset", () => { const model: LanguageModelChatMessage = { role: LanguageModelChatMessageRole.User, - content: "", + content: [{ value: "" }], name: undefined, }; diff --git a/packages/vscode-extension/test/officeChat/handlers.test.ts b/packages/vscode-extension/test/officeChat/handlers.test.ts index b4cb46e1764..d82ae4e48df 100644 --- a/packages/vscode-extension/test/officeChat/handlers.test.ts +++ b/packages/vscode-extension/test/officeChat/handlers.test.ts @@ -44,10 +44,10 @@ describe("File: officeChat/handlers.ts", () => { prompt: "test", command: OfficeChatCommand.Create, references: [], - location: 1, - attempt: 0, - enableCommandDetection: false, - } as vscode.ChatRequest; + toolReferences: [], + toolInvocationToken: 0, + model: { name: "gpt-4o" }, + } as unknown as vscode.ChatRequest; const officeCreateCommandHandlerStub = sandbox.stub(officeCreateCommandHandler, "default"); handler.officeChatRequestHandler( request, @@ -63,10 +63,13 @@ describe("File: officeChat/handlers.ts", () => { prompt: "test", command: OfficeChatCommand.GenerateCode, references: [], + toolReferences: [], + toolInvocationToken: 0, + model: { name: "gpt-4o" }, location: 1, attempt: 0, enableCommandDetection: false, - } as vscode.ChatRequest; + } as unknown as vscode.ChatRequest; const generatecodeCommandHandlerStub = sandbox.stub(generatecodeCommandHandler, "default"); handler.officeChatRequestHandler( request, @@ -82,10 +85,13 @@ describe("File: officeChat/handlers.ts", () => { prompt: "test", command: OfficeChatCommand.NextStep, references: [], + toolReferences: [], + toolInvocationToken: 0, + model: { name: "gpt-4o" }, location: 1, attempt: 0, enableCommandDetection: false, - } as vscode.ChatRequest; + } as unknown as vscode.ChatRequest; const officeNextStepCommandHandlerStub = sandbox.stub( officeNextStepCommandHandler, "default" @@ -104,10 +110,13 @@ describe("File: officeChat/handlers.ts", () => { prompt: "test", command: "", references: [], + toolReferences: [], + toolInvocationToken: 0, + model: { name: "gpt-4o" }, location: 1, attempt: 0, enableCommandDetection: false, - } as vscode.ChatRequest; + } as unknown as vscode.ChatRequest; const officeChatTelemetryDataMock = sandbox.createStubInstance(OfficeChatTelemetryData); sandbox.stub(officeChatTelemetryDataMock, "properties").get(function getterFn() { return undefined; @@ -135,10 +144,13 @@ describe("File: officeChat/handlers.ts", () => { prompt: "", command: "", references: [], + toolReferences: [], + toolInvocationToken: 0, + model: { name: "gpt-4o" }, location: 1, attempt: 0, enableCommandDetection: false, - } as vscode.ChatRequest; + } as unknown as vscode.ChatRequest; const officeChatTelemetryDataMock = sandbox.createStubInstance(OfficeChatTelemetryData); sandbox.stub(officeChatTelemetryDataMock, "properties").get(function getterFn() { return undefined; From e064b32606983ae8c99804a6fe3c1d267a5e8a1f Mon Sep 17 00:00:00 2001 From: Siyuan Chen <67082457+ayachensiyuan@users.noreply.github.com> Date: Thu, 25 Sep 2025 17:18:58 +0800 Subject: [PATCH 21/24] test: change chef bot sample path (#14594) Co-authored-by: ivanchen --- .github/workflows/ui-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ui-test.yml b/.github/workflows/ui-test.yml index 1f412937a9f..6319749b591 100644 --- a/.github/workflows/ui-test.yml +++ b/.github/workflows/ui-test.yml @@ -385,7 +385,7 @@ jobs: uses: actions/checkout@v3 with: repository: microsoft/teams-ai - ref: main + ref: js-1.7.4 path: packages/tests/resource - name: Download samples food catalog From df0464af7a3d44bddb654d7100526a959877b0ac Mon Sep 17 00:00:00 2001 From: Yuqi Zhou Date: Tue, 23 Sep 2025 22:02:50 +0800 Subject: [PATCH 22/24] refactor: csharp AI V2 readme --- .../.{{NewProjectTypeName}}/README.md.tpl | 2 +- .../.{{NewProjectTypeName}}/README.md.tpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/vs/csharp/custom-copilot-assistant-assistants-api/.{{NewProjectTypeName}}/README.md.tpl b/templates/vs/csharp/custom-copilot-assistant-assistants-api/.{{NewProjectTypeName}}/README.md.tpl index 9d75ff4ffe4..a18680e8d4a 100644 --- a/templates/vs/csharp/custom-copilot-assistant-assistants-api/.{{NewProjectTypeName}}/README.md.tpl +++ b/templates/vs/csharp/custom-copilot-assistant-assistants-api/.{{NewProjectTypeName}}/README.md.tpl @@ -109,7 +109,7 @@ You can follow [Build an AI Agent in Teams](https://aka.ms/teamsfx-ai-agent) to - [Add functions](https://aka.ms/teamsfx-ai-agent#add-functions-with-assistants-api) ## Additional information and references -- [Teams AI library](https://aka.ms/teams-ai-library) +- [Teams AI library V2](https://aka.ms/teams-ai-library-v2) - [Microsoft 365 Agents Toolkit Documentations](https://docs.microsoft.com/microsoftteams/platform/toolkit/teams-toolkit-fundamentals) - [Microsoft 365 Agents Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) - [Microsoft 365 Agents Toolkit Samples](https://github.com/OfficeDev/TeamsFx-Samples) diff --git a/templates/vs/csharp/custom-copilot-assistant-new/.{{NewProjectTypeName}}/README.md.tpl b/templates/vs/csharp/custom-copilot-assistant-new/.{{NewProjectTypeName}}/README.md.tpl index 46b8d94ed41..9f73147a15a 100644 --- a/templates/vs/csharp/custom-copilot-assistant-new/.{{NewProjectTypeName}}/README.md.tpl +++ b/templates/vs/csharp/custom-copilot-assistant-new/.{{NewProjectTypeName}}/README.md.tpl @@ -76,7 +76,7 @@ The app template is built using the Teams AI library, which provides the capabil You can follow [Get started with Teams AI library](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/teams%20conversational%20ai/how-conversation-ai-get-started) to extend the agent template with more AI capabilities. ## Additional information and references -- [Teams AI library](https://aka.ms/teams-ai-library) +- [Teams AI library V2](https://aka.ms/teams-ai-library-v2) - [Microsoft 365 Agents Toolkit Documentations](https://docs.microsoft.com/microsoftteams/platform/toolkit/teams-toolkit-fundamentals) - [Microsoft 365 Agents Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) - [Microsoft 365 Agents Toolkit Samples](https://github.com/OfficeDev/TeamsFx-Samples) From da75082a15b6804025d5316da5d2f305c12c483b Mon Sep 17 00:00:00 2001 From: Yuqi Zhou Date: Wed, 24 Sep 2025 07:40:23 +0800 Subject: [PATCH 23/24] refactor: csharp AI V2 readme --- .../.{{NewProjectTypeName}}/README.md.tpl | 2 +- .../.{{NewProjectTypeName}}/README.md.tpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/vs/csharp/custom-copilot-assistant-assistants-api/.{{NewProjectTypeName}}/README.md.tpl b/templates/vs/csharp/custom-copilot-assistant-assistants-api/.{{NewProjectTypeName}}/README.md.tpl index a18680e8d4a..9d75ff4ffe4 100644 --- a/templates/vs/csharp/custom-copilot-assistant-assistants-api/.{{NewProjectTypeName}}/README.md.tpl +++ b/templates/vs/csharp/custom-copilot-assistant-assistants-api/.{{NewProjectTypeName}}/README.md.tpl @@ -109,7 +109,7 @@ You can follow [Build an AI Agent in Teams](https://aka.ms/teamsfx-ai-agent) to - [Add functions](https://aka.ms/teamsfx-ai-agent#add-functions-with-assistants-api) ## Additional information and references -- [Teams AI library V2](https://aka.ms/teams-ai-library-v2) +- [Teams AI library](https://aka.ms/teams-ai-library) - [Microsoft 365 Agents Toolkit Documentations](https://docs.microsoft.com/microsoftteams/platform/toolkit/teams-toolkit-fundamentals) - [Microsoft 365 Agents Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) - [Microsoft 365 Agents Toolkit Samples](https://github.com/OfficeDev/TeamsFx-Samples) diff --git a/templates/vs/csharp/custom-copilot-assistant-new/.{{NewProjectTypeName}}/README.md.tpl b/templates/vs/csharp/custom-copilot-assistant-new/.{{NewProjectTypeName}}/README.md.tpl index 9f73147a15a..46b8d94ed41 100644 --- a/templates/vs/csharp/custom-copilot-assistant-new/.{{NewProjectTypeName}}/README.md.tpl +++ b/templates/vs/csharp/custom-copilot-assistant-new/.{{NewProjectTypeName}}/README.md.tpl @@ -76,7 +76,7 @@ The app template is built using the Teams AI library, which provides the capabil You can follow [Get started with Teams AI library](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/teams%20conversational%20ai/how-conversation-ai-get-started) to extend the agent template with more AI capabilities. ## Additional information and references -- [Teams AI library V2](https://aka.ms/teams-ai-library-v2) +- [Teams AI library](https://aka.ms/teams-ai-library) - [Microsoft 365 Agents Toolkit Documentations](https://docs.microsoft.com/microsoftteams/platform/toolkit/teams-toolkit-fundamentals) - [Microsoft 365 Agents Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) - [Microsoft 365 Agents Toolkit Samples](https://github.com/OfficeDev/TeamsFx-Samples) From 13d14a439bf4e8268d8dab5334bc3962cd629a89 Mon Sep 17 00:00:00 2001 From: Qinzhou Xu Date: Tue, 4 Nov 2025 13:34:12 +0800 Subject: [PATCH 24/24] fix: local debug and build issue --- .../vsc/common/declarative-agent-typespec/env/.env.local | 1 + .../declarative-agent-typespec/m365agents.local.yml.tpl | 7 ++++++- .../common/declarative-agent-typespec/m365agents.yml.tpl | 2 +- .../src/agent/actions/github.tsp | 9 ++++++++- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/templates/vsc/common/declarative-agent-typespec/env/.env.local b/templates/vsc/common/declarative-agent-typespec/env/.env.local index dff6cd82d95..29a4cbaa3eb 100644 --- a/templates/vsc/common/declarative-agent-typespec/env/.env.local +++ b/templates/vsc/common/declarative-agent-typespec/env/.env.local @@ -3,6 +3,7 @@ # Built-in environment variables TEAMSFX_ENV=local APP_NAME_SUFFIX=local +GITHUB_API_URL=https://api.github.com # Generated during provision, you can also add your own variables. TEAMS_APP_ID= diff --git a/templates/vsc/common/declarative-agent-typespec/m365agents.local.yml.tpl b/templates/vsc/common/declarative-agent-typespec/m365agents.local.yml.tpl index ad79b2b03f4..b63ceb13ae1 100644 --- a/templates/vsc/common/declarative-agent-typespec/m365agents.local.yml.tpl +++ b/templates/vsc/common/declarative-agent-typespec/m365agents.local.yml.tpl @@ -19,6 +19,11 @@ provision: with: args: install --no-audit --progress=false + - uses: cli/runNpmCommand + name: Generate TypeSpec environment variables + with: + args: run generate:env -- ${{TEAMSFX_ENV}} + # Compile typespec files and generate necessary files for agent. # If you want to update the outputDir, please make sure the following paths are also updated. # 1. File paths in tspconfig.yaml. @@ -26,7 +31,7 @@ provision: # 3. manifestPath in teamsApp/zipAppPackage action. Please set the value to the same as manifestPath in this action. - uses: typeSpec/compile with: - path: ./main.tsp + path: ./src/agent/main.tsp manifestPath: ./appPackage/manifest.json outputDir: ./appPackage/.generated typeSpecConfigPath: ./tspconfig.yaml diff --git a/templates/vsc/common/declarative-agent-typespec/m365agents.yml.tpl b/templates/vsc/common/declarative-agent-typespec/m365agents.yml.tpl index 653a2887aa2..d59ea682ae4 100644 --- a/templates/vsc/common/declarative-agent-typespec/m365agents.yml.tpl +++ b/templates/vsc/common/declarative-agent-typespec/m365agents.yml.tpl @@ -96,7 +96,7 @@ publish: # 3. manifestPath in teamsApp/zipAppPackage action. Please set the value to the same as manifestPath in this action. - uses: typeSpec/compile with: - path: ./main.tsp + path: ./src/agent/main.tsp manifestPath: ./appPackage/manifest.json outputDir: ./appPackage/.generated typeSpecConfigPath: ./tspconfig.yaml diff --git a/templates/vsc/common/declarative-agent-typespec/src/agent/actions/github.tsp b/templates/vsc/common/declarative-agent-typespec/src/agent/actions/github.tsp index 4659aa8f919..c9a95243d27 100644 --- a/templates/vsc/common/declarative-agent-typespec/src/agent/actions/github.tsp +++ b/templates/vsc/common/declarative-agent-typespec/src/agent/actions/github.tsp @@ -31,7 +31,14 @@ namespace GitHubAPI { * @param per_page The number of results per page. Default is 5. */ @route("/search/issues") - @card(#{ dataPath: "$.items", title: "$.title", url: "$.html_url", file: "adaptiveCards/searchIssues.json" }) + @card(#{ + dataPath: "$.items", + file: "adaptiveCards/searchIssues.json", + properties: #{ + title: "$.title", + url: "$.html_url" + } + }) @get op searchIssues( @query q: string = "repo:OfficeDev/microsoft-365-agents-toolkit is:issue is:open", @query per_page: integer = 5