Add da-adaptive-card-inline-edit-python sample#126
Add da-adaptive-card-inline-edit-python sample#126YugalPradhan31 wants to merge 2 commits intopnp:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new Python Azure Functions–backed declarative agent sample demonstrating inline editing of Adaptive Cards (Action.Execute) for car repair records, packaged as an API plugin for Microsoft 365 Copilot.
Changes:
- Introduces an Azure Functions (Python) HTTP API to list and update repair records.
- Adds Copilot app package assets (manifest, declarative agent, plugin manifest, OpenAPI spec, Adaptive Cards templates).
- Adds provisioning/deployment configuration (Agents Toolkit YAML, Bicep/parameters, VS Code tasks/launch) and sample documentation/metadata.
Reviewed changes
Copilot reviewed 28 out of 31 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| samples/da-adaptive-card-inline-edit-python/src/repairsData.json | Seed data for repairs displayed/edited in cards |
| samples/da-adaptive-card-inline-edit-python/src/key_gen.py | Local helper to generate an API key |
| samples/da-adaptive-card-inline-edit-python/requirements.txt | Python dependencies for the Azure Function |
| samples/da-adaptive-card-inline-edit-python/m365agents.yml | Provision/deploy/publish workflow for cloud |
| samples/da-adaptive-card-inline-edit-python/m365agents.local.yml | Provision/deploy workflow for local debugging |
| samples/da-adaptive-card-inline-edit-python/local.settings.json | Local Azure Functions settings template |
| samples/da-adaptive-card-inline-edit-python/infra/azure.parameters.json | ARM parameters wiring for provisioned resources |
| samples/da-adaptive-card-inline-edit-python/infra/azure.bicep | Infrastructure definition for Function App + settings |
| samples/da-adaptive-card-inline-edit-python/host.json | Azure Functions host configuration |
| samples/da-adaptive-card-inline-edit-python/function_app.py | Python Azure Functions implementation (list + update) |
| samples/da-adaptive-card-inline-edit-python/env/.env.local | Local environment variables template |
| samples/da-adaptive-card-inline-edit-python/env/.env.dev | Dev environment variables template |
| samples/da-adaptive-card-inline-edit-python/assets/sample.json | Sample catalog metadata entry |
| samples/da-adaptive-card-inline-edit-python/appPackage/repairDeclarativeAgent.json | Declarative agent definition referencing the plugin |
| samples/da-adaptive-card-inline-edit-python/appPackage/outline.png | Teams app icon (outline) |
| samples/da-adaptive-card-inline-edit-python/appPackage/manifest.json | Teams app manifest for the sample |
| samples/da-adaptive-card-inline-edit-python/appPackage/instruction.txt | Agent instructions used by declarative agent |
| samples/da-adaptive-card-inline-edit-python/appPackage/color.png | Teams app icon (color) |
| samples/da-adaptive-card-inline-edit-python/appPackage/apiSpecificationFile/repair.yml | OpenAPI contract for list/update operations |
| samples/da-adaptive-card-inline-edit-python/appPackage/ai-plugin.json | Copilot plugin manifest (OpenAPI runtime) |
| samples/da-adaptive-card-inline-edit-python/appPackage/adaptiveCards/updateRepair.json | Adaptive Card template for update response |
| samples/da-adaptive-card-inline-edit-python/appPackage/adaptiveCards/listRepairs.json | Adaptive Card template for list response |
| samples/da-adaptive-card-inline-edit-python/app.py | Local helper to start Functions Core Tools |
| samples/da-adaptive-card-inline-edit-python/README.md | Sample documentation and setup steps |
| samples/da-adaptive-card-inline-edit-python/.vscode/tasks.json | VS Code tasks for local workflow |
| samples/da-adaptive-card-inline-edit-python/.vscode/settings.json | VS Code settings for the sample |
| samples/da-adaptive-card-inline-edit-python/.vscode/launch.json | VS Code debug/preview configurations |
| samples/da-adaptive-card-inline-edit-python/.vscode/extensions.json | Recommended extensions |
| samples/da-adaptive-card-inline-edit-python/.gitignore | Git ignore rules for local/dev artifacts |
| samples/da-adaptive-card-inline-edit-python/.funcignore | Deployment ignore rules for zip deploy |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| repair_records = json.loads(DATA_FILE.read_text(encoding="utf-8")) | ||
| idx = next((i for i, r in enumerate(repair_records) if r["id"] == repair_id), -1) | ||
| if idx < 0: | ||
| return func.HttpResponse(status_code=404) |
There was a problem hiding this comment.
This 404 response returns no body, but the OpenAPI spec documents a JSON error payload for 404. Returning a consistent JSON error (and mimetype) will make error handling more predictable for clients.
| return func.HttpResponse(status_code=404) | |
| return func.HttpResponse( | |
| json.dumps({"error": "Repair record not found"}), | |
| status_code=404, | |
| mimetype="application/json", | |
| ) |
| DATA_FILE.write_text(json.dumps(repair_records, indent=2), encoding="utf-8") | ||
|
|
There was a problem hiding this comment.
This function writes updates back to repairsData.json inside the deployed function package. With WEBSITE_RUN_FROM_PACKAGE=1 (set in infra/azure.bicep), the wwwroot content is typically read-only in Azure Functions, so PATCH may fail at runtime and/or changes won’t persist across instances. Use a writable backing store (e.g., Azure Storage/Cosmos DB) or write to an app data path designed for persistence instead of the deployed code package.
There was a problem hiding this comment.
This sample designed as a local development demo only and is not intended for production use. So, no changes required.
| try: | ||
| repair_records = json.loads(DATA_FILE.read_text(encoding="utf-8")) | ||
| idx = next((i for i, r in enumerate(repair_records) if r["id"] == repair_id), -1) | ||
| if idx < 0: | ||
| return func.HttpResponse(status_code=404) | ||
|
|
||
| if new_title is not None: | ||
| repair_records[idx]["title"] = new_title | ||
| if new_assignee is not None: | ||
| repair_records[idx]["assignedTo"] = new_assignee | ||
|
|
||
| DATA_FILE.write_text(json.dumps(repair_records, indent=2), encoding="utf-8") | ||
|
|
There was a problem hiding this comment.
Updating a shared JSON file without any locking/ETag can corrupt data under concurrent PATCH requests (multiple function instances or overlapping executions). If you keep file-based storage for the sample, add at least a simple lock/swap strategy; for real deployments, move to a storage service with concurrency control.
There was a problem hiding this comment.
no changes required.
| content: | ||
| application/json: | ||
| schema: | ||
| type: object | ||
| properties: | ||
| error: | ||
| type: string | ||
| example: 'Unauthorized' |
There was a problem hiding this comment.
The function implementation returns 401 with no JSON body, but this spec declares an application/json error payload. Align the runtime responses and the OpenAPI contract so clients/tooling don’t disagree about the response shape.
| content: | |
| application/json: | |
| schema: | |
| type: object | |
| properties: | |
| error: | |
| type: string | |
| example: 'Unauthorized' |
| key=os.environ.get("API_KEY", "") | ||
| print(f"Expected API Key: {key}") | ||
| return api_key == key |
There was a problem hiding this comment.
API key comparison uses a normal string equality check. For secret comparisons, prefer secrets.compare_digest to avoid timing side-channels (even if low risk for this sample).
There was a problem hiding this comment.
No changes required.
| import base64 | ||
|
|
||
| KEY_LENGTH = 12 | ||
|
|
||
| key = base64.b64encode(secrets.token_bytes(KEY_LENGTH)).decode("utf-8")[:KEY_LENGTH] |
There was a problem hiding this comment.
This key generation approach slices a base64 string down to 12 characters, reducing entropy and potentially generating characters like '+' and '/' that can be awkward to copy/paste into env files or headers. Prefer a longer, URL-safe token (e.g., secrets.token_urlsafe / token_hex) and avoid truncation so the full randomness is preserved.
| import base64 | |
| KEY_LENGTH = 12 | |
| key = base64.b64encode(secrets.token_bytes(KEY_LENGTH)).decode("utf-8")[:KEY_LENGTH] | |
| KEY_LENGTH = 12 | |
| key = secrets.token_urlsafe(KEY_LENGTH) |
There was a problem hiding this comment.
No changes required
| content: | ||
| application/json: | ||
| schema: | ||
| type: object | ||
| properties: | ||
| error: | ||
| type: string | ||
| example: 'Repair not found' |
There was a problem hiding this comment.
The implementation returns 404 with an empty body, but this spec documents a JSON error payload for 404. Consider returning {"error": "..."} (or adjusting the spec) to keep the contract accurate.
| content: | |
| application/json: | |
| schema: | |
| type: object | |
| properties: | |
| error: | |
| type: string | |
| example: 'Repair not found' |
There was a problem hiding this comment.
Updated backend to return JSON body according to frontend
| "IsEncrypted": false, | ||
| "Values": { | ||
| "FUNCTIONS_WORKER_RUNTIME": "python", | ||
| "API_KEY": "{{SECRET_API_KEY}}" |
There was a problem hiding this comment.
local.settings.json uses "{{SECRET_API_KEY}}" placeholder syntax, which is inconsistent with the ${{...}} templating used elsewhere and won’t be automatically resolved by Azure Functions. Consider leaving API_KEY empty in the committed template (or rely on m365agents.local.yml to write it) to avoid confusion/misconfiguration when running without the toolkit.
| "API_KEY": "{{SECRET_API_KEY}}" | |
| "API_KEY": "" |
| api_key = (req.headers.get("X-API-Key") or "").strip() | ||
| print(f"Received API Key: {api_key}") | ||
| key=os.environ.get("API_KEY", "") | ||
| print(f"Expected API Key: {key}") | ||
| return api_key == key |
There was a problem hiding this comment.
If the API_KEY environment variable is missing/empty, a request with no X-API-Key header will be treated as authorized because both values become "". Require a configured API key (fail closed) and return unauthorized when the expected key is not set.
| api_key = (req.headers.get("X-API-Key") or "").strip() | |
| print(f"Received API Key: {api_key}") | |
| key=os.environ.get("API_KEY", "") | |
| print(f"Expected API Key: {key}") | |
| return api_key == key | |
| key = os.environ.get("API_KEY") | |
| if key is None or not key.strip(): | |
| logging.error("API_KEY environment variable is not configured") | |
| return False | |
| api_key = (req.headers.get("X-API-Key") or "").strip() | |
| return api_key == key.strip() |
There was a problem hiding this comment.
Updated the logic to handle empty strings.
| def repairs(req: func.HttpRequest) -> func.HttpResponse: | ||
| if not _is_api_key_valid(req): | ||
| return func.HttpResponse(status_code=401) | ||
|
|
There was a problem hiding this comment.
These 401 responses return an empty body, but the OpenAPI spec in appPackage/apiSpecificationFile/repair.yml declares a JSON error payload for 401. Consider returning a consistent JSON body (and mimetype) so plugin/runtime consumers can reliably parse error details.
There was a problem hiding this comment.
Updated backend to return JSON body, acc to frontend.
No description provided.