New Sample - Allow Inline Editing for Adaptive Card Responses in Microsoft 365 Copilot Declarative Agents#105
Conversation
…osoft 365 Copilot Declarative Agents
There was a problem hiding this comment.
Pull request overview
This PR introduces a new sample demonstrating inline editing of Adaptive Card responses in Microsoft 365 Copilot declarative agents. The sample showcases a car repair tracking system where users can view and edit repair records directly within Copilot using Action.Execute from Adaptive Cards v1.5.
Changes:
- Implements an Azure Functions backend with GET and PATCH endpoints for car repair records
- Provides API key authentication and OpenAPI specification for plugin integration
- Includes Adaptive Card templates with inline editing capabilities using Action.Execute
- Adds infrastructure configuration (Bicep templates) for Azure deployment
Reviewed changes
Copilot reviewed 27 out of 31 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| src/functions/repair.js | Core API logic for listing and updating repair records with API key authentication |
| src/keyGen.js | Utility script for generating API keys |
| src/repairsData.json | Sample data file containing car repair records |
| package.json, package-lock.json | Project dependencies including Azure Functions SDK |
| appPackage/apiSpecificationFile/repair.yml | OpenAPI 3.0 specification defining the Repair Service API |
| appPackage/adaptiveCards/*.json | Adaptive Card templates for displaying and editing repair records |
| appPackage/manifest.json | Teams app manifest configuration |
| appPackage/repairDeclarativeAgent.json | Declarative agent definition with instructions and conversation starters |
| infra/azure.bicep | Azure infrastructure as code for Function App deployment |
| m365agents.yml, m365agents.local.yml | Build and deployment configuration for local and production environments |
| README.md | Comprehensive documentation with setup instructions and feature explanations |
Files not reviewed (1)
- samples/da-adaptive-card-inline-edit/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| description: The user who is responsible for the repair | ||
| date: | ||
| type: string | ||
| format: date-time |
There was a problem hiding this comment.
The OpenAPI specification defines the date field with format 'date-time', but the actual data in repairsData.json uses simple date format (e.g., "2023-05-23") without time component. This is a discrepancy between the API specification and the actual data format. Either update the format to 'date' or ensure the data includes time components in ISO 8601 format (e.g., "2023-05-23T00:00:00Z").
| format: date-time | |
| format: date |
| ## Prerequisites | ||
|
|
||
| * [Microsoft 365 account with Copilot access](https://www.microsoft.com/microsoft-365/enterprise/copilot-for-microsoft-365) | ||
| * [Node.js](https://nodejs.org) version 18.x, 20.x, or 22.x |
There was a problem hiding this comment.
The README states that Node.js version 18.x, 20.x, or 22.x is supported, but this is inconsistent with the actual dependency requirements. The @azure/functions 4.10.0 package requires Node.js >=20.0, so Node.js 18.x will not work. Update this to state "Node.js version 20.x or 22.x" to accurately reflect the actual requirements.
| * [Node.js](https://nodejs.org) version 18.x, 20.x, or 22.x | |
| * [Node.js](https://nodejs.org) version 20.x or 22.x |
| const filePath = path.join(__dirname, '../repairsData.json'); | ||
| const fileData = fs.readFileSync(filePath, 'utf-8'); | ||
| const repairs = JSON.parse(fileData); | ||
| const idx = repairs.findIndex(r => r.id === id); | ||
| if (idx < 0) { | ||
| return { status: 404 }; | ||
| } | ||
| if (newTitle !== undefined) repairs[idx].title = newTitle; | ||
| if (newAssignee !== undefined) repairs[idx].assignedTo = newAssignee; | ||
| fs.writeFileSync(filePath, JSON.stringify(repairs, null, 2)); |
There was a problem hiding this comment.
The updateRepair function has a critical race condition issue. Multiple concurrent requests can read the same file state, modify it, and write it back, resulting in lost updates. Consider using a database or implementing proper file locking mechanisms. For a production application, using synchronous file operations without locking is not safe for concurrent write operations.
There was a problem hiding this comment.
This is simple file-based approach, which is suitable for demonstration and single-user scenarios only. For production applications, we have to use database.
| const body = await req.json(); | ||
| const newTitle = body.title; | ||
| const newAssignee = body.assignedTo; | ||
| const filePath = path.join(__dirname, '../repairsData.json'); | ||
| const fileData = fs.readFileSync(filePath, 'utf-8'); | ||
| const repairs = JSON.parse(fileData); | ||
| const idx = repairs.findIndex(r => r.id === id); | ||
| if (idx < 0) { | ||
| return { status: 404 }; | ||
| } | ||
| if (newTitle !== undefined) repairs[idx].title = newTitle; | ||
| if (newAssignee !== undefined) repairs[idx].assignedTo = newAssignee; |
There was a problem hiding this comment.
Missing input validation in the updateRepair function. The function should validate that newTitle and newAssignee are strings and not empty when provided. Without validation, malicious or malformed input could corrupt the data store or cause unexpected behavior.
There was a problem hiding this comment.
Updated to validate newTitle and newAssignee
| } | ||
| { | ||
| name: 'WEBSITE_NODE_DEFAULT_VERSION' | ||
| value: '~18' // Set NodeJS version to 18.x |
There was a problem hiding this comment.
The Azure Bicep template specifies Node.js version ~18 for the Azure Function App, but this conflicts with the @azure/functions 4.10.0 dependency which requires Node.js >=20.0. This will cause the application to fail when deployed to Azure. Update this value to '~20' or higher to match the actual dependency requirements.
| value: '~18' // Set NodeJS version to 18.x | |
| value: '~20' // Set NodeJS version to 20.x to match @azure/functions requirement |
| type: object | ||
| properties: | ||
| updatedRepair: | ||
| $ref: '#/components/schemas/Repair' |
There was a problem hiding this comment.
The OpenAPI specification for the PATCH endpoint does not define error responses (401 Unauthorized, 404 Not Found, 500 Internal Server Error) even though the implementation returns these status codes. Add response definitions for '401' (missing/invalid API key), '404' (repair not found), and '500' (server error) to accurately document the API behavior.
| $ref: '#/components/schemas/Repair' | |
| $ref: '#/components/schemas/Repair' | |
| '401': | |
| description: Missing or invalid API key | |
| content: | |
| application/json: | |
| schema: | |
| type: object | |
| properties: | |
| code: | |
| type: string | |
| description: Error code | |
| message: | |
| type: string | |
| description: Description of the authentication error | |
| '404': | |
| description: Repair not found | |
| content: | |
| application/json: | |
| schema: | |
| type: object | |
| properties: | |
| code: | |
| type: string | |
| description: Error code | |
| message: | |
| type: string | |
| description: Details about the missing repair | |
| '500': | |
| description: Internal server error | |
| content: | |
| application/json: | |
| schema: | |
| type: object | |
| properties: | |
| code: | |
| type: string | |
| description: Error code | |
| message: | |
| type: string | |
| description: Description of the server error |
There was a problem hiding this comment.
Updated to match with server responses.
| const fileData = fs.readFileSync(filePath, 'utf-8'); | ||
| const repairs = JSON.parse(fileData); | ||
| const idx = repairs.findIndex(r => r.id === id); | ||
| if (idx < 0) { | ||
| return { status: 404 }; | ||
| } | ||
| if (newTitle !== undefined) repairs[idx].title = newTitle; | ||
| if (newAssignee !== undefined) repairs[idx].assignedTo = newAssignee; | ||
| fs.writeFileSync(filePath, JSON.stringify(repairs, null, 2)); | ||
| return { | ||
| status: 200, | ||
| jsonBody: { updatedRepair: repairs[idx] } | ||
| }; |
There was a problem hiding this comment.
The updateRepair function lacks error handling for file I/O operations and JSON parsing. If the file write fails or JSON parsing fails, the function will throw an unhandled exception. Consider wrapping these operations in try-catch blocks and returning appropriate error responses.
| const fileData = fs.readFileSync(filePath, 'utf-8'); | |
| const repairs = JSON.parse(fileData); | |
| const idx = repairs.findIndex(r => r.id === id); | |
| if (idx < 0) { | |
| return { status: 404 }; | |
| } | |
| if (newTitle !== undefined) repairs[idx].title = newTitle; | |
| if (newAssignee !== undefined) repairs[idx].assignedTo = newAssignee; | |
| fs.writeFileSync(filePath, JSON.stringify(repairs, null, 2)); | |
| return { | |
| status: 200, | |
| jsonBody: { updatedRepair: repairs[idx] } | |
| }; | |
| try { | |
| const fileData = fs.readFileSync(filePath, 'utf-8'); | |
| const repairs = JSON.parse(fileData); | |
| const idx = repairs.findIndex(r => r.id === id); | |
| if (idx < 0) { | |
| return { status: 404 }; | |
| } | |
| if (newTitle !== undefined) repairs[idx].title = newTitle; | |
| if (newAssignee !== undefined) repairs[idx].assignedTo = newAssignee; | |
| fs.writeFileSync(filePath, JSON.stringify(repairs, null, 2)); | |
| return { | |
| status: 200, | |
| jsonBody: { updatedRepair: repairs[idx] } | |
| }; | |
| } catch (err) { | |
| if (context && typeof context.log === "function") { | |
| context.log("Error updating repair record:", err); | |
| } | |
| return { | |
| status: 500, | |
| jsonBody: { error: "Failed to update repair record." } | |
| }; | |
| } |
There was a problem hiding this comment.
Added try-catch blocks and returning appropriate error responses.
| const fullName = item.assignedTo.toLowerCase(); | ||
| const [firstName, lastName] = fullName.split(" "); | ||
| return fullName === query || firstName === query || lastName === query; |
There was a problem hiding this comment.
The filter logic assumes that all assignedTo values contain exactly two words (first and last name). If an assignedTo field contains more than two names or a single name, the filtering logic may produce unexpected results. Consider using a more robust string matching approach or documenting this limitation.
| const fullName = item.assignedTo.toLowerCase(); | |
| const [firstName, lastName] = fullName.split(" "); | |
| return fullName === query || firstName === query || lastName === query; | |
| const fullName = String(item.assignedTo || "").trim().toLowerCase(); | |
| if (!fullName) { | |
| return false; | |
| } | |
| const nameParts = fullName.split(/\s+/); | |
| return fullName === query || nameParts.includes(query); |
There was a problem hiding this comment.
Updated filter logic to first check if full name matches query, if not split name into words and check if query matches any word
garrytrinder
left a comment
There was a problem hiding this comment.
Schema version review - see inline comments
| @@ -0,0 +1,18 @@ | |||
| { | |||
| "$schema": "https://developer.microsoft.com/json-schemas/copilot/declarative-agent/v1.5/schema.json", | |||
| "version": "v1.5", | |||
There was a problem hiding this comment.
The declarative agent schema should be updated to the latest version (v1.6). Current: v1.5. See: https://learn.microsoft.com/en-us/microsoft-365-copilot/extensibility/declarative-agent-manifest
There was a problem hiding this comment.
Updated version to v1.6
| @@ -0,0 +1,62 @@ | |||
| { | |||
| "$schema": "https://developer.microsoft.com/json-schemas/copilot/plugin/v2.2/schema.json", | |||
| "schema_version": "v2.2", | |||
There was a problem hiding this comment.
The API plugin schema should be updated to the latest version (v2.4). Current: v2.2. See: https://learn.microsoft.com/en-us/microsoft-365-copilot/extensibility/api-plugin-manifest
There was a problem hiding this comment.
Updated version to v2.2
There was a problem hiding this comment.
Thanks for the sample @Harikrishnan-MSFT! A few things need addressing before merge.
|
|
||
| Version|Date|Author|Comments | ||
| -------|----|----|-------- | ||
| 1.0|January 20, 2026|Microsoft|Initial release |
There was a problem hiding this comment.
Author should list actual contributor name(s), not "Microsoft".
There was a problem hiding this comment.
Added actual contributor's name.
| - [Adaptive Cards Schema Explorer](https://adaptivecards.io/explorer/) | ||
| - [API Plugins for Microsoft 365 Copilot](https://learn.microsoft.com/microsoft-365-copilot/extensibility/overview-api-plugins) | ||
|
|
||
| <img src="https://m365-visitor-stats.azurewebsites.net/copilot-pro-dev-samples/samples/da-adaptive-card-inline-edit" /> |
There was a problem hiding this comment.
Tracking image should use markdown format per template:
|
@Harikrishnan-MSFT are you able to provide an update? When you have resolved the comments, please mark this PR as ready for review, thank you! |
No description provided.