diff --git a/samples/da-SnowWizard/README.md b/samples/da-SnowWizard/README.md new file mode 100644 index 0000000..12f21c7 --- /dev/null +++ b/samples/da-SnowWizard/README.md @@ -0,0 +1,183 @@ +# Copilot Snow Wizard + +## Summary + +This sample project demonstrates an implementation of an M365 Declarative Copilot that interfaces with ServiceNow to list and create incidents. + +| ![CopilotSnowWizard Screenshot 1](assets/2024-10-11_16-29.png) | ![CopilotSnowWizard Screenshot 1](assets/2024-10-11_16-39.png) | ![CopilotSnowWizard Screenshot 1](assets/2024-10-11_16-40.png) | +|:-------------------------------------------------------------:|:-------------------------------------------------------------:|:-------------------------------------------------------------:| + + +## Contributors + + +* [Cristiano Goncalves](https://github.com/cristianoag) +* [Luis Demetrio](https://github.com/luishdemetrio) + +## Version history + +Version|Date|Comments +-------|----|-------- +1.0|October 11, 2024|Initial release + +## Prerequisites + +* Microsoft 365 tenant with Microsoft 365 Copilot +* Visual Studio COde +* Teams Toolkit +* Node.js +* ServiceNow Developer Instance + +## Minimal path to awesome + +In order to run the code, you will need an M365 tenant with Copilot licenses enabled. Additionally, you must enable a policy to sideload custom applications. Without this, you will not be able to run the code. These instructions assume you have admin rights on the tenant. If you do not have admin rights, please work with your administrator to allow the upload of custom applications. + +### Enable Teams custom application uploads + +By default, end users can't upload applications directly; an Administrator needs to upload them into the enterprise app catalog. Follow these steps to enable direct uploads via Teams Toolkit: + +1. Navigate to the [Microsoft 365 Admin Center](https://admin.microsoft.com/). + +2. In the left panel, select **Show all** to expand the navigation menu. Then, select **Teams** to open the Microsoft Teams admin center. + +3. In the Teams admin center, expand the **Teams apps** section and select **Setup policies**. You will see a list of App setup policies. Select the **Global (Org-wide default)** policy. + +4. Ensure the **Upload custom apps** switch is turned **On**. + +5. Scroll down and select the **Save** button to apply your changes. + +Next, you need to install the following software on your computer to use the sample code: + +1. [Visual Studio Code](https://code.visualstudio.com/download) +2. [Node.js](https://nodejs.org/en/download/) +3. [Teams Toolkit](https://marketplace.visualstudio.com/items?itemName=TeamsDevApp.ms-teams-vscode-extension) + +### Creating the ServiceNow Personal Developer Instance + +To use the sample code provided, you will need a ServiceNow Personal Developer Instance (PDI). Follow the instructions below to create your instance and familiarize yourself with the basics of ServiceNow: + +1. **Sign in to the [ServiceNow Developer Site](https://developer.servicenow.com/dev.do).** +2. In the header, click the **Request Instance** button. +3. Select a ServiceNow release for your instance. +4. Click the **Request** button. +5. When your instance is ready, a dialog will provide the URL and admin login details. Copy the current password to a safe location for future use. +6. Click the **Open Instance** button to open your instance in a new browser tab. + +To open your instance later, sign in to the Developer Site, open the Account menu, and click the **Start Building** button. + +ServiceNow offers free PDIs to registered users who want to develop applications or improve their skills on the ServiceNow platform. New Developer Program members automatically receive a PDI running the latest release, which remains active as long as there is activity on the instance. + +It is strongly recommended to acquire basic knowledge of ServiceNow concepts. This will help you understand how M365 Copilot interfaces with ServiceNow. You can follow a basic learning path at [New to ServiceNow | ServiceNow Developers](https://developer.servicenow.com/dev.do#!/learn/learning-plans/washingtondc/new_to_servicenow/). + +For more detailed instructions and learning resources, visit [ServiceNow Basics Objectives | ServiceNow Developers](https://developer.servicenow.com/dev.do#!/learn/learning-plans/washingtondc/new_to_servicenow/app_store_learnv2_buildmyfirstapp_washingtondc_servicenow_basics_objectives). + +### Download the Sample Code from GitHub + +If you are familiar with Git, you can clone this repository and open the `SnowWizard` folder in Visual Studio Code. If not, you can download a ZIP file containing all the content by clicking the green "Code" button at the top right corner of the repository page. + +If you download the ZIP file, uncompress it on your PC and use Visual Studio Code to open the `SnowWizard` folder. + +### Configuring the Declarative Copilot + +Once you downloaded the code and opened in with Visual Studio Code you will need to create a configuration file and provide some config information in order to make the declarative agent to interface with your ServiceNow development instance. + +On Visual Studio Code, create a file named .env.local.user inside the env folder. + +### ServiceNow Configuration + +Create a `.env.local.user` file inside the `env` folder with the following content: + +``` +SN_INSTANCE='devxxxxxx' +SN_USERNAME='user' +SN_PASSWORD='user_password' +``` + +**Note:** +- The `SN_INSTANCE` value should be the initial part of your ServiceNow development instance URL. +- This sample code uses user credentials to interface with ServiceNow. Ensure the user has the necessary permissions to list and create incidents in ServiceNow. +- Store your credentials securely and avoid sharing them publicly. + +### Install the Node.js requisites + +Open a Command Prompt and navigate to the SnowWizard folder. Run NPM INSTALL and wait until all the libraries and requirements get installed. + +### Run the Declarative Agent + +Now on Visual Studio, click on the Teams Toolkit icon on the left rail and sign-in with your M365 tenant credentials. You do that on the Accounts section by providing your M365 tenant credentials. + +Now click Preview Your Teams App (F5) and enjoy. + +### Sample Prompts + +``` +List all ServiceNow incidents +Create a table with all ServiceNow open incidents +Create a ServiceNow incident with jokes as short and full descriptions +Create a ServiceNow incident based on the content of the text below. +``` + +Please note that the code is limiting the return from the ServiceNow interface in just 10 incidents. That can be easily changed by adjusting the limit on the service interface implemented by snow_incidents.ts. + + +## Features + +List and creates Service Now incidents + + + + + +## Help + + + +We do not support samples, but this community is always willing to help, and we want to improve these samples. We use GitHub to track issues, which makes it easy for community members to volunteer their time and help resolve issues. + +You can try looking at [issues related to this sample](https://github.com/pnp/copilot-pro-dev-samples/issues?q=label%3A%22sample%3A%20YOUR-SOLUTION-NAME%22) to see if anybody else is having the same issues. + +If you encounter any issues using this sample, [create a new issue](https://github.com/pnp/copilot-pro-dev-samples/issues/new). + +Finally, if you have an idea for improvement, [make a suggestion](https://github.com/pnp/copilot-pro-dev-samples/issues/new). + +## Disclaimer + +**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.** + +![](https://m365-visitor-stats.azurewebsites.net/SamplesGallery/da-SnowWizard) diff --git a/samples/da-SnowWizard/SnowWizard/.funcignore b/samples/da-SnowWizard/SnowWizard/.funcignore new file mode 100644 index 0000000..8af9cc6 --- /dev/null +++ b/samples/da-SnowWizard/SnowWizard/.funcignore @@ -0,0 +1,21 @@ +.funcignore +*.js.map +*.ts +.git* +.localConfigs +.vscode +local.settings.json +test +tsconfig.json +.DS_Store +.deployment +node_modules/.bin +node_modules/azure-functions-core-tools +README.md +tsconfig.json +teamsapp.yml +teamsapp.*.yml +/env/ +/appPackage/ +/infra/ +/devTools/ \ No newline at end of file diff --git a/samples/da-SnowWizard/SnowWizard/.gitignore b/samples/da-SnowWizard/SnowWizard/.gitignore new file mode 100644 index 0000000..bb0a54c --- /dev/null +++ b/samples/da-SnowWizard/SnowWizard/.gitignore @@ -0,0 +1,30 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. +# TeamsFx files +env/.env.*.user +# env/.env.local +.DS_Store +build +appPackage/build +.deployment + +# dependencies +/node_modules + +# testing +/coverage + +# Dev tool directories +/devTools/ + +# TypeScript output +dist +out + +# Azure Functions artifacts +bin +obj +appsettings.json +# local.settings.json + +# Local data +.localConfigs \ No newline at end of file diff --git a/samples/da-SnowWizard/SnowWizard/.vscode/extensions.json b/samples/da-SnowWizard/SnowWizard/.vscode/extensions.json new file mode 100644 index 0000000..aac0a6e --- /dev/null +++ b/samples/da-SnowWizard/SnowWizard/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "TeamsDevApp.ms-teams-vscode-extension" + ] +} diff --git a/samples/da-SnowWizard/SnowWizard/.vscode/launch.json b/samples/da-SnowWizard/SnowWizard/.vscode/launch.json new file mode 100644 index 0000000..910cce1 --- /dev/null +++ b/samples/da-SnowWizard/SnowWizard/.vscode/launch.json @@ -0,0 +1,97 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch App in Teams (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://www.office.com/chat?auth=2", + "cascadeTerminateToConfigurations": [ + "Attach to Backend" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen", + "perScriptSourcemaps": "yes" + }, + { + "name": "Launch App in Teams (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://www.office.com/chat?auth=2", + "cascadeTerminateToConfigurations": [ + "Attach to Backend" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen", + "perScriptSourcemaps": "yes" + }, + { + "name": "Preview in Copilot (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://www.office.com/chat?auth=2", + "presentation": { + "group": "remote", + "order": 1 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Preview in Copilot (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://www.office.com/chat?auth=2", + "presentation": { + "group": "remote", + "order": 2 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Attach to Backend", + "type": "node", + "request": "attach", + "port": 9229, + "restart": true, + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + } + ], + "compounds": [ + { + "name": "Debug in Copilot (Edge)", + "configurations": [ + "Launch App in Teams (Edge)", + "Attach to Backend" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "all", + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug in Copilot (Chrome)", + "configurations": [ + "Launch App in Teams (Chrome)", + "Attach to Backend" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "all", + "order": 2 + }, + "stopAll": true + } + ] +} diff --git a/samples/da-SnowWizard/SnowWizard/.vscode/settings.json b/samples/da-SnowWizard/SnowWizard/.vscode/settings.json new file mode 100644 index 0000000..0ed7b2e --- /dev/null +++ b/samples/da-SnowWizard/SnowWizard/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "debug.onTaskErrors": "abort", + "json.schemas": [ + { + "fileMatch": [ + "/aad.*.json" + ], + "schema": {} + } + ], + "azureFunctions.stopFuncTaskPostDebug": false, + "azureFunctions.showProjectWarning": false, +} diff --git a/samples/da-SnowWizard/SnowWizard/.vscode/tasks.json b/samples/da-SnowWizard/SnowWizard/.vscode/tasks.json new file mode 100644 index 0000000..dbc7dc2 --- /dev/null +++ b/samples/da-SnowWizard/SnowWizard/.vscode/tasks.json @@ -0,0 +1,129 @@ +// This file is automatically generated by Teams Toolkit. +// The teamsfx tasks defined in this file require Teams Toolkit version >= 5.0.0. +// See https://aka.ms/teamsfx-tasks for details on how to customize each task. +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Start Teams App Locally", + "dependsOn": [ + "Validate prerequisites", + "Start local tunnel", + "Create resources", + "Build project", + "Start application" + ], + "dependsOrder": "sequence" + }, + { + "label": "Validate prerequisites", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", + "m365Account", + "portOccupancy" + ], + "portOccupancy": [ + 7071, + 9229 + ] + } + }, + { + // Start the local tunnel service to forward public URL to local port and inspect traffic. + // See https://aka.ms/teamsfx-tasks/local-tunnel for the detailed args definitions. + "label": "Start local tunnel", + "type": "teamsfx", + "command": "debug-start-local-tunnel", + "args": { + "type": "dev-tunnel", + "ports": [ + { + "portNumber": 7071, + "protocol": "http", + "access": "public", + "writeToEnvironmentFile": { + "endpoint": "OPENAPI_SERVER_URL", // output tunnel endpoint as OPENAPI_SERVER_URL + } + } + ], + "env": "local" + }, + "isBackground": true, + "problemMatcher": "$teamsfx-local-tunnel-watch" + }, + { + "label": "Create resources", + "type": "teamsfx", + "command": "provision", + "args": { + "env": "local" + } + }, + { + "label": "Build project", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "local" + } + }, + { + "label": "Start application", + "dependsOn": [ + "Start backend" + ] + }, + { + "label": "Start backend", + "type": "shell", + "command": "npm run dev:teamsfx", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}", + "env": { + "PATH": "${workspaceFolder}/devTools/func:${env:PATH}" + } + }, + "windows": { + "options": { + "env": { + "PATH": "${workspaceFolder}/devTools/func;${env:PATH}" + } + } + }, + "problemMatcher": { + "pattern": { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + }, + "background": { + "activeOnStart": true, + "beginsPattern": "^.*(Job host stopped|signaling restart).*$", + "endsPattern": "^.*(Worker process started and initialized|Host lock lease acquired by instance ID).*$" + } + }, + "presentation": { + "reveal": "silent" + }, + "dependsOn": "Watch backend" + }, + { + "label": "Watch backend", + "type": "shell", + "command": "npm run watch:teamsfx", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": "$tsc-watch", + "presentation": { + "reveal": "silent" + } + } + ] +} \ No newline at end of file diff --git a/samples/da-SnowWizard/SnowWizard/appPackage/SnowWizardDeclarativeAgent.json b/samples/da-SnowWizard/SnowWizard/appPackage/SnowWizardDeclarativeAgent.json new file mode 100644 index 0000000..a03d5f4 --- /dev/null +++ b/samples/da-SnowWizard/SnowWizard/appPackage/SnowWizardDeclarativeAgent.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://aka.ms/json-schemas/copilot/declarative-agent/v1.0/schema.json", + "version": "v1.0", + "name": "Snow Wizard ${{APP_NAME_SUFFIX}}", + "description": "This M365 Copilot declarative agent interfaces with ServiceNow to enable AI-driven automation. It allows the Copilot to retrieve and process data from Service Now, providing users with intelligent, conversational interaction for executing tasks and managing workflows seamlessly through an AI-powered chatbot. This agent supports both content retrieval and action execution within Service Now, enhancing productivity and efficiency in service management operations.", + "instructions": "$[file('instruction.txt')]", + "conversation_starters": [ + { + "text": "List all ServiceNow incidents" + }, + { + "text": "Create a ServiceNow incident with the 'The 3rd floor printer is not working' short description" + } + ], + "actions": [ + { + "id": "SnowWizard", + "file": "SnowWizardPlugin.json" + } + ] +} \ No newline at end of file diff --git a/samples/da-SnowWizard/SnowWizard/appPackage/SnowWizardPlugin.json b/samples/da-SnowWizard/SnowWizard/appPackage/SnowWizardPlugin.json new file mode 100644 index 0000000..7afeec6 --- /dev/null +++ b/samples/da-SnowWizard/SnowWizard/appPackage/SnowWizardPlugin.json @@ -0,0 +1,97 @@ +{ + "schema_version": "v2.1", + "name_for_human": "SnowWizard", + "namespace": "SnowWizard", + "description_for_human": "API to list, create and update ServiceNow incidents", + "description_for_model": "Plugin for listing, creating and updating ServiceNow incidents, you can list, create or update ServiceNow incidents.", + "functions": [ + { + "name": "listIncidents", + "description": "Returns a detailed list of incidents from ServiceNow with their details.", + "capabilities": { + "response_semantics": { + "data_path": "$.results", + "properties": { + "title": "$.title", + "subtitle": "$.description" + }, + "static_template": { + "type": "AdaptiveCard", + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.5", + "body": [ + { + "type": "Container", + "$data": "${$root}", + "items": [ + { + "type": "TextBlock", + "text": "Number: ${if(number, number, 'N/A')}", + "wrap": true + }, + { + "type": "TextBlock", + "text": "Short Description: ${if(short_description, short_description, 'N/A')}", + "wrap": true + }, + { + "type": "TextBlock", + "text": "Priority: ${if(priority, priority, 'N/A')}", + "wrap": true + }, + { + "type": "TextBlock", + "text": "Opened: ${if(sys_created_on, sys_created_on, 'N/A')}", + "wrap": true + } + ] + } + ] + } + } + } + }, + { + "name": "createIncident", + "description": "Creates a new incident in ServiceNow.", + "capabilities": { + "response_semantics": { + "data_path": "$.results", + "properties": { + "title": "$.title", + "subtitle": "$.description" + } + }, + "confirmation": { + "type": "AdaptiveCard", + "title": "Create a new ServiceNow incident on behalf of the logged user?", + "body": "* **Incident**: {{function.parameters.short_description}}\n*" + } + } + } + ], + "runtimes": [ + { + "type": "OpenApi", + "auth": { + "type": "None" + }, + "spec": { + "url": "apiSpecificationFile/incident.yml", + "progress_style": "ShowUsageWithInputAndOutput" + }, + "run_for_functions": ["listIncidents", "createIncident"] + } + ], + "capabilities": { + "localization": {}, + "conversation_starters": [ + { + "text": "List all ServiceNow incidents" + }, + { + "text": "Create a ServiceNow incident with the 'The 3rd floor printer is not working' short description" + } + ] + } +} diff --git a/samples/da-SnowWizard/SnowWizard/appPackage/apiSpecificationFile/incident.yml b/samples/da-SnowWizard/SnowWizard/appPackage/apiSpecificationFile/incident.yml new file mode 100644 index 0000000..393c9ec --- /dev/null +++ b/samples/da-SnowWizard/SnowWizard/appPackage/apiSpecificationFile/incident.yml @@ -0,0 +1,102 @@ +openapi: 3.0.0 +info: + title: ServiceNow Data Service + description: A simple service to manage ServiceNow data, including incidents. + version: 1.0.0 +servers: + - url: ${{OPENAPI_SERVER_URL}}/api + description: The ServiceNow data service api server +paths: + /incidents/: + get: + operationId: listIncidents + summary: List incidents from ServiceNow + description: Returns a list of incidents from ServiceNow with their details and images + parameters: + - name: opened_by + in: query + description: Filter incidents by who opened them + schema: + type: string + required: false + responses: + '200': + description: A list of incidents returned by ServiceNow + content: + application/json: + schema: + type: object + properties: + results: + type: array + items: + type: object + properties: + priority: + type: string + description: Priority level of the incident. + number: + type: string + description: Unique identifier for the incident. + short_description: + type: string + description: Short description of the incident. + description: + type: string + description: Detailed description of the incident. + opened_at: + type: string + format: date-time + description: Timestamp of when the incident was opened. + made_sla: + type: string + enum: ["true", "false"] + description: Indicates if the incident met the Service Level Agreement (SLA). + post: + operationId: createIncident + summary: Create a new incident in ServiceNow + description: Creates a new incident in ServiceNow with the provided details + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + short_description: + type: string + description: Short description of the incident. + description: + type: string + description: Detailed description of the incident. + responses: + '201': + description: ServiceNow Incident created successfully + content: + application/json: + schema: + type: object + properties: + result: + type: object + properties: + priority: + type: string + description: Priority level of the incident. + number: + type: string + description: Unique identifier for the incident. + short_description: + type: string + description: Short description of the incident. + description: + type: string + description: Detailed description of the incident. + opened_at: + type: string + format: date-time + description: Timestamp of when the incident was opened. + made_sla: + type: string + enum: ["true", "false"] + description: Indicates if the incident met the Service Level Agreement (SLA). diff --git a/samples/da-SnowWizard/SnowWizard/appPackage/color.png b/samples/da-SnowWizard/SnowWizard/appPackage/color.png new file mode 100644 index 0000000..1868c04 Binary files /dev/null and b/samples/da-SnowWizard/SnowWizard/appPackage/color.png differ diff --git a/samples/da-SnowWizard/SnowWizard/appPackage/instruction.txt b/samples/da-SnowWizard/SnowWizard/appPackage/instruction.txt new file mode 100644 index 0000000..b930b0b --- /dev/null +++ b/samples/da-SnowWizard/SnowWizard/appPackage/instruction.txt @@ -0,0 +1,8 @@ +Great the user with a joke about Snow and telling that you are here to supporting them. + +You will help the user to interface with ServiceNow through the SnowWizard action, listing, creating and updating incidents. +You can filter incidents based on specific person that had the incident assigned to, the name of the person should be provided by the user. +You can also list all incidents if requested. The user may provide the name of the person and you will need to understand the user's intent and provide the incident records assigned to that person. +You can also create incidents. The user will provide information about the incident and you will use the SnowWizard acrion to create it. You can also create multiple incidents by calling the function multiple times. + +You have access to documents stored on the SharePoint and when asked about ServiceNow leverage data from the SnowWizard action. \ No newline at end of file diff --git a/samples/da-SnowWizard/SnowWizard/appPackage/manifest.json b/samples/da-SnowWizard/SnowWizard/appPackage/manifest.json new file mode 100644 index 0000000..27b3ef0 --- /dev/null +++ b/samples/da-SnowWizard/SnowWizard/appPackage/manifest.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/teams/vDevPreview/MicrosoftTeams.schema.json", + "manifestVersion": "devPreview", + "id": "${{TEAMS_APP_ID}}", + "version": "1.0.0", + "developer": { + "name": "Teams App, Inc.", + "websiteUrl": "https://www.example.com", + "privacyUrl": "https://www.example.com/privacy", + "termsOfUseUrl": "https://www.example.com/termsofuse" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "Snow Wizard ${{APP_NAME_SUFFIX}}", + "full": "Empower Service Now with M365 Copilot: Smarter Actions, Seamless Workflows..." + }, + "description": { + "short": "Seamless Service Now automation powered by M365 Copilot AI.", + "full": "Transform your Service Now experience with M365 Copilot. Leverage AI-powered automation to access, manage, and execute tasks effortlessly through an intelligent chatbot, boosting productivity and streamlining workflows across your organization." + }, + "accentColor": "#FFFFFF", + "copilotExtensions": { + "declarativeCopilots": [ + { + "id": "SnowWizardDeclarativeAgent", + "file": "SnowWizardDeclarativeAgent.json" + } + ] + }, + "permissions": [ + "identity", + "messageTeamMembers" + ] +} diff --git a/samples/da-SnowWizard/SnowWizard/appPackage/outline.png b/samples/da-SnowWizard/SnowWizard/appPackage/outline.png new file mode 100644 index 0000000..0fe01e2 Binary files /dev/null and b/samples/da-SnowWizard/SnowWizard/appPackage/outline.png differ diff --git a/samples/da-SnowWizard/SnowWizard/env/.env.dev b/samples/da-SnowWizard/SnowWizard/env/.env.dev new file mode 100644 index 0000000..342a8af --- /dev/null +++ b/samples/da-SnowWizard/SnowWizard/env/.env.dev @@ -0,0 +1,17 @@ +# This file includes environment variables that will be committed to git by default. + +# Built-in environment variables +TEAMSFX_ENV=dev +APP_NAME_SUFFIX=dev + +# Updating AZURE_SUBSCRIPTION_ID or AZURE_RESOURCE_GROUP_NAME after provision may also require an update to RESOURCE_SUFFIX, because some services require a globally unique name across subscriptions/resource groups. +AZURE_SUBSCRIPTION_ID= +AZURE_RESOURCE_GROUP_NAME= +RESOURCE_SUFFIX= + +# Generated during provision, you can also add your own variables. +TEAMS_APP_ID= +TEAMS_APP_PUBLISHED_APP_ID= +TEAMS_APP_TENANT_ID= +API_FUNCTION_ENDPOINT= +API_FUNCTION_RESOURCE_ID= \ No newline at end of file diff --git a/samples/da-SnowWizard/SnowWizard/env/.env.local b/samples/da-SnowWizard/SnowWizard/env/.env.local new file mode 100644 index 0000000..5a050f3 --- /dev/null +++ b/samples/da-SnowWizard/SnowWizard/env/.env.local @@ -0,0 +1,5 @@ +# This file includes environment variables that can be committed to git. It's gitignored by default because it represents your local development environment. + +# Built-in environment variables +TEAMSFX_ENV=local +APP_NAME_SUFFIX=local diff --git a/samples/da-SnowWizard/SnowWizard/host.json b/samples/da-SnowWizard/SnowWizard/host.json new file mode 100644 index 0000000..06d01bd --- /dev/null +++ b/samples/da-SnowWizard/SnowWizard/host.json @@ -0,0 +1,15 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[4.*, 5.0.0)" + } +} diff --git a/samples/da-SnowWizard/SnowWizard/http/SnowWizardAPI.http b/samples/da-SnowWizard/SnowWizard/http/SnowWizardAPI.http new file mode 100644 index 0000000..e369a98 --- /dev/null +++ b/samples/da-SnowWizard/SnowWizard/http/SnowWizardAPI.http @@ -0,0 +1,29 @@ +@base_url = http://localhost:7071/api + +### Get all incidents +{{base_url}}/incidents + +### Get a specific incident +{{base_url}}/incidents/INC0000060 + +### Create an incident +POST {{base_url}}/incidents +Content-Type: application/json + +{ + "short_description":"This is a test incident short description", + "description":"This is a test incident long description" +} + + +### Get all my incidents +{{base_url}}/incidents/me + +### Get an user profile +{{base_url}}/profiles/beth.anglin@example.com + +### Get another user profile +{{base_url}}/profiles/fred.luddy@example.com + +### Get my profile +{{base_url}}/me \ No newline at end of file diff --git a/samples/da-SnowWizard/SnowWizard/infra/azure.bicep b/samples/da-SnowWizard/SnowWizard/infra/azure.bicep new file mode 100644 index 0000000..8021c1d --- /dev/null +++ b/samples/da-SnowWizard/SnowWizard/infra/azure.bicep @@ -0,0 +1,57 @@ +@maxLength(20) +@minLength(4) +param resourceBaseName string +param functionAppSKU string + +param location string = resourceGroup().location +param serverfarmsName string = resourceBaseName +param functionAppName string = resourceBaseName + +// Compute resources for Azure Functions +resource serverfarms 'Microsoft.Web/serverfarms@2021-02-01' = { + name: serverfarmsName + location: location + sku: { + name: functionAppSKU // You can follow https://aka.ms/teamsfx-bicep-add-param-tutorial to add functionServerfarmsSku property to provisionParameters to override the default value "Y1". + } + properties: {} +} + +// Azure Functions that hosts your function code +resource functionApp 'Microsoft.Web/sites@2021-02-01' = { + name: functionAppName + kind: 'functionapp' + location: location + properties: { + serverFarmId: serverfarms.id + httpsOnly: true + siteConfig: { + appSettings: [ + { + name: 'FUNCTIONS_EXTENSION_VERSION' + value: '~4' // Use Azure Functions runtime v4 + } + { + name: 'FUNCTIONS_WORKER_RUNTIME' + value: 'node' // Set runtime to NodeJS + } + { + name: 'WEBSITE_RUN_FROM_PACKAGE' + value: '1' // Run Azure Functions from a package file + } + { + name: 'WEBSITE_NODE_DEFAULT_VERSION' + value: '~18' // Set NodeJS version to 18.x + } + ] + ftpsState: 'FtpsOnly' + } + } +} +var apiEndpoint = 'https://${functionApp.properties.defaultHostName}' + + +// The output will be persisted in .env.{envName}. Visit https://aka.ms/teamsfx-actions/arm-deploy for more details. +output API_FUNCTION_ENDPOINT string = apiEndpoint +output API_FUNCTION_RESOURCE_ID string = functionApp.id +output OPENAPI_SERVER_URL string = apiEndpoint diff --git a/samples/da-SnowWizard/SnowWizard/infra/azure.parameters.json b/samples/da-SnowWizard/SnowWizard/infra/azure.parameters.json new file mode 100644 index 0000000..ede6521 --- /dev/null +++ b/samples/da-SnowWizard/SnowWizard/infra/azure.parameters.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceBaseName": { + "value": "plugin${{RESOURCE_SUFFIX}}" + }, + "functionAppSKU": { + "value": "Y1" + } + } +} diff --git a/samples/da-SnowWizard/SnowWizard/local.settings.json b/samples/da-SnowWizard/SnowWizard/local.settings.json new file mode 100644 index 0000000..7e3601c --- /dev/null +++ b/samples/da-SnowWizard/SnowWizard/local.settings.json @@ -0,0 +1,6 @@ +{ + "IsEncrypted": false, + "Values": { + "FUNCTIONS_WORKER_RUNTIME": "node" + } +} \ No newline at end of file diff --git a/samples/da-SnowWizard/SnowWizard/package-lock.json b/samples/da-SnowWizard/SnowWizard/package-lock.json new file mode 100644 index 0000000..d1fb74f --- /dev/null +++ b/samples/da-SnowWizard/SnowWizard/package-lock.json @@ -0,0 +1,312 @@ +{ + "name": "snowwizard", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "snowwizard", + "version": "1.0.0", + "dependencies": { + "@azure/functions": "^4.3.0", + "axios": "^1.7.7", + "dotenv": "^16.4.5" + }, + "devDependencies": { + "@types/node": "^18.11.9", + "env-cmd": "^10.1.0", + "typescript": "^4.1.6" + } + }, + "node_modules/@azure/functions": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@azure/functions/-/functions-4.5.1.tgz", + "integrity": "sha512-ikiw1IrM2W9NlQM3XazcX+4Sq3XAjZi4eeG22B5InKC2x5i7MatGF2S/Gn1ACZ+fEInwu+Ru9J8DlnBv1/hIvg==", + "license": "MIT", + "dependencies": { + "cookie": "^0.6.0", + "long": "^4.0.0", + "undici": "^5.13.0" + }, + "engines": { + "node": ">=18.0" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@types/node": { + "version": "18.19.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.55.tgz", + "integrity": "sha512-zzw5Vw52205Zr/nmErSEkN5FLqXPuKX/k5d1D7RKHATGqU7y6YfX9QxZraUzUrFGqH6XzOzG196BC35ltJC4Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/env-cmd": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/env-cmd/-/env-cmd-10.1.0.tgz", + "integrity": "sha512-mMdWTT9XKN7yNth/6N6g2GuKuJTsKMDHlQFUDacb/heQRRWOTIZ42t1rMHnQu4jYxU1ajdTeJM+9eEETlqToMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^4.0.0", + "cross-spawn": "^7.0.0" + }, + "bin": { + "env-cmd": "bin/env-cmd.js" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "license": "Apache-2.0" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + } + } +} diff --git a/samples/da-SnowWizard/SnowWizard/package.json b/samples/da-SnowWizard/SnowWizard/package.json new file mode 100644 index 0000000..938b983 --- /dev/null +++ b/samples/da-SnowWizard/SnowWizard/package.json @@ -0,0 +1,25 @@ +{ + "name": "snowwizard", + "version": "1.0.0", + "scripts": { + "dev:teamsfx": "env-cmd --silent -f .localConfigs npm run dev", + "dev": "func start --typescript --language-worker=\"--inspect=9229\" --port \"7071\" --cors \"*\"", + "build": "tsc", + "watch:teamsfx": "tsc --watch", + "watch": "tsc -w", + "prestart": "npm run build", + "start": "npx func start", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "@azure/functions": "^4.3.0", + "axios": "^1.7.7", + "dotenv": "^16.4.5" + }, + "devDependencies": { + "@types/node": "^18.11.9", + "env-cmd": "^10.1.0", + "typescript": "^4.1.6" + }, + "main": "dist/src/functions/*.js" +} diff --git a/samples/da-SnowWizard/SnowWizard/src/functions/incidents.ts b/samples/da-SnowWizard/SnowWizard/src/functions/incidents.ts new file mode 100644 index 0000000..d4ddb0a --- /dev/null +++ b/samples/da-SnowWizard/SnowWizard/src/functions/incidents.ts @@ -0,0 +1,89 @@ + +import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions"; +import { HttpError } from "../services/utilities"; +import IncidentsApiService from "../services/snow_incidents"; + +/** + * This function handles the HTTP request and returns the incidents information. + * + * @param {HttpRequest} req - The HTTP request. + * @param {InvocationContext} context - The Azure Functions context object. + * @returns {Promise} - A promise that resolves with the HTTP response containing the incident information. + */ +export async function incidents( + req: HttpRequest, + context: InvocationContext +): Promise { + + // Initialize response. + const res: HttpResponseInit = { + status: 200, + jsonBody: { + results: [], + }, + }; + + try { + // Get input parameters. + const id = req.params?.id; // Incident ID + // Need to implement authentication to get the address from context, for now lets use Fred Luddy as the current user + const email = 'fred.luddy@example.com' + let body = null; + + switch (req.method) { + case "GET": { + + if (id) { + // Fetch the incident from the ServiceNow API. + console.log(`➡️ GET /api/incidents/${id}: `); + const incident = await IncidentsApiService.getIncident(id); + res.jsonBody.results = incident ?? []; + console.log(` ✅ GET /api/incidents${id}: response status ${res.status}; ${incident.length} incidents returned`); + return res; + } + + // Fetch all incidents from the ServiceNow API. + console.log(`➡️ GET /api/incidents: `); + const incidents = await IncidentsApiService.getIncidents(); + res.jsonBody.results = incidents ?? []; + console.log(` ✅ GET /api/incidents: response status ${res.status}; ${incidents.length} incidents returned`); + return res; + + } + case "POST": { + try { + const bd = await req.text(); + body = JSON.parse(bd); + } catch (error) { + throw new HttpError(400, `No body to process this request.`); + } + if (body) { + // Create a new incident in ServiceNow. + console.log(`➡️ POST /api/incidents: `); + const incident = await IncidentsApiService.createIncident(email, body["short_description"], body["description"]); + res.jsonBody.results = incident ?? []; + console.log(` ✅ POST /api/incidents: response status ${res.status}; ${incident.number} incident created!`); + return res; + } + } + default: { + throw new Error(`Method not allowed: ${req.method}`); + } + } + + } + catch (error) { + console.error(` ❌ GET /api/incidents: ${error}`); + res.status = 500; + res.jsonBody = { error: error.message }; + return res; + } + +} + +app.http("incidents", { + methods: ["GET", "POST"], + authLevel: "anonymous", + route: "incidents/{*id}", + handler: incidents, +}); diff --git a/samples/da-SnowWizard/SnowWizard/src/functions/me.ts b/samples/da-SnowWizard/SnowWizard/src/functions/me.ts new file mode 100644 index 0000000..4057e56 --- /dev/null +++ b/samples/da-SnowWizard/SnowWizard/src/functions/me.ts @@ -0,0 +1,51 @@ +import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions"; +import ProfilesApiService from "../services/snow_profiles" + +/** + * This function handles the HTTP request and returns the profile information. + * + * @param {HttpRequest} req - The HTTP request. + * @param {InvocationContext} context - The Azure Functions context object. + * @returns {Promise} - A promise that resolves with the HTTP response containing the profile information. + */ + +export async function profiles( + req: HttpRequest, + context: InvocationContext + ): Promise { + + // Initialize response. + const res: HttpResponseInit = { + status: 200, + jsonBody: { + results: [], + }, + }; + + try { + // Need to implement authentication to get the address from context, for now lets use Fred Luddy as the current user + const email = 'fred.luddy@example.com' + + console.log(`➡️ GET /api/me: `); + + // Fetch the profile from the ServiceNow API. + const profile = await ProfilesApiService.getProfile(email); + res.jsonBody.results = profile ?? []; + console.log(` ✅ GET /api/me: response status ${res.status}; ${profile.length} profiles returned`); + return res; + + } + catch (error) { + console.error(` ❌ GET /api/me: ${error}`); + res.status = 500; + res.jsonBody = { error: error.message }; + return res; + } +} + +app.http("me", { + methods: ["GET"], + authLevel: "anonymous", + route: "me/{*command}", + handler: profiles, + }); \ No newline at end of file diff --git a/samples/da-SnowWizard/SnowWizard/src/functions/profiles.ts b/samples/da-SnowWizard/SnowWizard/src/functions/profiles.ts new file mode 100644 index 0000000..98cfaa6 --- /dev/null +++ b/samples/da-SnowWizard/SnowWizard/src/functions/profiles.ts @@ -0,0 +1,52 @@ +import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions"; +import ProfilesApiService from "../services/snow_profiles" + +/** + * This function handles the HTTP request and returns the profile information. + * + * @param {HttpRequest} req - The HTTP request. + * @param {InvocationContext} context - The Azure Functions context object. + * @returns {Promise} - A promise that resolves with the HTTP response containing the profile information. + */ + +export async function profiles( + req: HttpRequest, + context: InvocationContext + ): Promise { + + // Initialize response. + const res: HttpResponseInit = { + status: 200, + jsonBody: { + results: [], + }, + }; + + try { + // Get input parameters. + //const email = req.query.get("email"); + const email = req.params?.email?.toLowerCase(); + + console.log(`➡️ GET /api/profiles: `); + + // Fetch the profile from the ServiceNow API. + const profile = await ProfilesApiService.getProfile(email); + res.jsonBody.results = profile ?? []; + console.log(` ✅ GET /api/profiles: response status ${res.status}; ${profile.length} profiles returned`); + return res; + + } + catch (error) { + console.error(` ❌ GET /api/profiles: ${error}`); + res.status = 500; + res.jsonBody = { error: error.message }; + return res; + } +} + +app.http("profiles", { + methods: ["GET"], + authLevel: "anonymous", + route: "profiles/{*email}", + handler: profiles, + }); \ No newline at end of file diff --git a/samples/da-SnowWizard/SnowWizard/src/incidents_data.json b/samples/da-SnowWizard/SnowWizard/src/incidents_data.json new file mode 100644 index 0000000..a93034d --- /dev/null +++ b/samples/da-SnowWizard/SnowWizard/src/incidents_data.json @@ -0,0 +1,50 @@ +[ + { + "number": "1", + "title": "Oil change", + "description": "Need to drain the old engine oil and replace it with fresh oil to keep the engine lubricated and running smoothly.", + "assignedTo": "Karin Blair", + "date": "2023-05-23", + "image": "https://www.howmuchisit.org/wp-content/uploads/2011/01/oil-change.jpg" + }, + { + "number": "2", + "title": "Brake repairs", + "description": "Conduct brake repairs, including replacing worn brake pads, resurfacing or replacing brake rotors, and repairing or replacing other components of the brake system.", + "assignedTo": "Issac Fielder", + "date": "2023-05-24", + "image": "https://upload.wikimedia.org/wikipedia/commons/7/71/Disk_brake_dsc03680.jpg" + }, + { + "number": "3", + "title": "Tire service", + "description": "Rotate and replace tires, moving them from one position to another on the vehicle to ensure even wear and removing worn tires and installing new ones.", + "assignedTo": "Karin Blair", + "date": "2023-05-24", + "image": "https://th.bing.com/th/number/OIP.N64J4jmqmnbQc5dHvTm-QAHaE8?pnumber=ImgDet&rs=1" + }, + { + "number": "4", + "title": "Battery replacement", + "description": "Remove the old battery and install a new one to ensure that the vehicle start reliably and the electrical systems function properly.", + "assignedTo": "Ashley McCarthy", + "date": "2023-05-25", + "image": "https://i.stack.imgur.com/4ftuj.jpg" + }, + { + "number": "5", + "title": "Engine tune-up", + "description": "This can include a variety of services such as replacing spark plugs, air filters, and fuel filters to keep the engine running smoothly and efficiently.", + "assignedTo": "Karin Blair", + "date": "2023-05-28", + "image": "https://th.bing.com/th/number/R.e4c01dd9f232947e6a92beb0a36294a5?rik=P076LRx7J6Xnrg&riu=http%3a%2f%2fupload.wikimedia.org%2fwikipedia%2fcommons%2ff%2ff3%2f1990_300zx_engine.jpg&ehk=f8KyT78eO3b%2fBiXzh6BZr7ze7f56TWgPST%2bY%2f%2bHqhXQ%3d&risl=&pnumber=ImgRaw&r=0" + }, + { + "number": "6", + "title": "Suspension and steering repairs", + "description": "This can include repairing or replacing components of the suspension and steering systems to ensure that the vehicle handles and rnumberes smoothly.", + "assignedTo": "Daisy Phillips", + "date": "2023-05-29", + "image": "https://i.stack.imgur.com/4v5OI.jpg" + } +] diff --git a/samples/da-SnowWizard/SnowWizard/src/model/basemodel.ts b/samples/da-SnowWizard/SnowWizard/src/model/basemodel.ts new file mode 100644 index 0000000..f516a3f --- /dev/null +++ b/samples/da-SnowWizard/SnowWizard/src/model/basemodel.ts @@ -0,0 +1,10 @@ +export interface Incident { + number: string; + name: string; + description: string; + clientName: string; + clientContact: string; + clientEmail: string; + location: Location; + mapUrl: string; +} \ No newline at end of file diff --git a/samples/da-SnowWizard/SnowWizard/src/services/snow_incidents.ts b/samples/da-SnowWizard/SnowWizard/src/services/snow_incidents.ts new file mode 100644 index 0000000..0e9519d --- /dev/null +++ b/samples/da-SnowWizard/SnowWizard/src/services/snow_incidents.ts @@ -0,0 +1,169 @@ +//Uses the ServiceNow Rest API to deal with incidents +//Author: crisag@microsoft.com + +import axios from 'axios'; +import * as dotenv from 'dotenv'; + +dotenv.config({ path: 'env/.env.local.user' }); + +class IncidentsApiService { + + private SN_INSTANCE: string; + private SN_USERNAME: string; + private SN_PASSWORD: string; + + constructor() { + // Environment variables setup + this.SN_INSTANCE = process.env.SN_INSTANCE || ''; + this.SN_USERNAME = process.env.SN_USERNAME || ''; + this.SN_PASSWORD = process.env.SN_PASSWORD || ''; + } + + // Function to fetch a single incident from ServiceNow + async getIncident(id: string) { + try { + const response = await axios.get( + `https://${this.SN_INSTANCE}.service-now.com/api/now/table/incident`, + { + params: { + sysparm_limit: 1, + sysparm_fields: 'number,made_sla,short_description,description,priority,opened_at', + sysparm_query: `number=${id}` + }, + auth: { + username: this.SN_USERNAME, + password: this.SN_PASSWORD + }, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + console.log('Incidents fetched successfully from ServiceNow:', response.data.result); + // Extracting incidents from response + return response.data.result; + } catch (error) { + console.error('Error fetching incidents:', error); + throw error; + } + } + + // Function to fetch the latest 10 incidents from ServiceNow + async getIncidents() { + try { + const response = await axios.get( + `https://${this.SN_INSTANCE}.service-now.com/api/now/table/incident`, + { + params: { + sysparm_limit: 10, + sysparm_fields: 'number,made_sla,short_description,description,priority,opened_at', + sysparm_query: 'ORDERBYDESCsys_created_on' + }, + auth: { + username: this.SN_USERNAME, + password: this.SN_PASSWORD + }, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + console.log('Incidents fetched successfully from ServiceNow:', response.data.result); + // Extracting incidents from response + return response.data.result; + } catch (error) { + console.error('Error fetching incidents:', error); + throw error; + } + } + + // Function to fetch the latest 10 incidents from ServiceNow + async getUserIncidents(username: string) { + try { + //get the sys_id of the user + const sys_id = await this.getUserSysId(username); + //fetch incidents assigned to the user + const response = await axios.get( + `https://${this.SN_INSTANCE}.service-now.com/api/now/table/incident`, + { + params: { + sysparm_limit: 10, + sysparm_fields: 'number,made_sla,short_description,description,priority,opened_at', + sysparm_query: `ORDERBYDESCsys_created_on^assigned_to=${sys_id}` + }, + auth: { + username: this.SN_USERNAME, + password: this.SN_PASSWORD + }, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + console.log('Incidents fetched successfully from ServiceNow:', response.data.result); + // Extracting incidents from response + return response.data.result; + } catch (error) { + console.error('Error fetching incidents:', error); + throw error; + } + } + + // Function to create a new incident in ServiceNow + async createIncident(email: string, short_description: string, description: string) { + try { + //get the sys_id of the user + const sys_id = await this.getUserSysId(email); + // Create a new incident on Service Now + const response = await axios.post( + `https://${this.SN_INSTANCE}.service-now.com/api/now/table/incident`, + { + short_description: `${short_description}`, + description: `${description}`, + caller_id: sys_id + }, + { + auth: { + username: this.SN_USERNAME, + password: this.SN_PASSWORD + }, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + console.log('Incident created successfully in ServiceNow:', response.data.result); + // Extracting incident from response + return response.data.result; + } catch (error) { + console.error('Error creating incident:', error); + throw error; + } + } + + private async getUserSysId(username: string) { + const response = await axios.get( + `https://${this.SN_INSTANCE}.service-now.com/api/now/table/sys_user`, + { + params: { + sysparm_limit: 10, + sysparm_query: `email=${username}` + }, + auth: { + username: this.SN_USERNAME, + password: this.SN_PASSWORD + }, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + console.log('User fetched successfully from ServiceNow:', response.data.result); + // Extracting user sys_id from response + return response.data.result[0].sys_id; + } + +} + +export default new IncidentsApiService(); + diff --git a/samples/da-SnowWizard/SnowWizard/src/services/snow_profiles.ts b/samples/da-SnowWizard/SnowWizard/src/services/snow_profiles.ts new file mode 100644 index 0000000..8f88c32 --- /dev/null +++ b/samples/da-SnowWizard/SnowWizard/src/services/snow_profiles.ts @@ -0,0 +1,53 @@ +//Uses the ServiceNow Rest API to deal with profile information +//Author: crisag@microsoft.com + +import axios from 'axios'; +import * as dotenv from 'dotenv'; + +dotenv.config({ path: 'env/.env.local.user' }); + +class ProfilesApiService { + + private SN_INSTANCE: string; + private SN_USERNAME: string; + private SN_PASSWORD: string; + + constructor() { + // Environment variables setup + this.SN_INSTANCE = process.env.SN_INSTANCE || ''; + this.SN_USERNAME = process.env.SN_USERNAME || ''; + this.SN_PASSWORD = process.env.SN_PASSWORD || ''; + } + + // Function to fetch the latest 10 incidents from ServiceNow + async getProfile(email: string) { + try { + const response = await axios.get( + `https://${this.SN_INSTANCE}.service-now.com/api/now/table/sys_user`, + { + params: { + sysparm_limit: 10, + sysparm_query: `email=${email}` + }, + auth: { + username: this.SN_USERNAME, + password: this.SN_PASSWORD + }, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + console.log('Profile fetched successfully from ServiceNow:', response.data.result); + return response.data.result; + } catch (error) { + console.error('Error fetching profile:', error); + throw error; + } + } + + +} + +export default new ProfilesApiService(); + diff --git a/samples/da-SnowWizard/SnowWizard/src/services/utilities.ts b/samples/da-SnowWizard/SnowWizard/src/services/utilities.ts new file mode 100644 index 0000000..fb36d90 --- /dev/null +++ b/samples/da-SnowWizard/SnowWizard/src/services/utilities.ts @@ -0,0 +1,33 @@ +// Throw this object to return an HTTP error +export class HttpError extends Error { + status: number; + constructor(status: number, message: string) { + super(message); + this.status = status; + } +} + +// Clean up common issues with Copilot parameters +export function cleanUpParameter(name: string, value: string): string { + + let val = value.toLowerCase(); + if (val.toLowerCase().includes("trey") || val.toLowerCase().includes("research")) { + const newVal = val.replace("trey", "").replace("research", "").trim(); + console.log(` ❗ Plugin name detected in the ${name} parameter '${val}'; replacing with '${newVal}'.`); + val = newVal; + } + if (val === "") { + console.log(` ❗ Invalid name '${val}'; replacing with 'avery'.`); + val = "avery"; + } + if (name==="role" && val === "consultant") { + console.log(` ❗ Invalid role name '${val}'; replacing with ''.`); + val = ""; + } + if (val === "null") { + console.log(` ❗ Invalid value '${val}'; replacing with ''.`); + val = ""; + } + return val; + +} diff --git a/samples/da-SnowWizard/SnowWizard/teamsapp.local.yml b/samples/da-SnowWizard/SnowWizard/teamsapp.local.yml new file mode 100644 index 0000000..fea833d --- /dev/null +++ b/samples/da-SnowWizard/SnowWizard/teamsapp.local.yml @@ -0,0 +1,73 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.7/yaml.schema.json +# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file +# Visit https://aka.ms/teamsfx-actions for details on actions +version: v1.7 + +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: Snow Wizard${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Set required variables for local launch + - uses: script + with: + run: + echo "::set-teamsfx-env FUNC_NAME=incidents"; + echo "::set-teamsfx-env FUNC_ENDPOINT=http://localhost:7071"; + + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputFolder: ./appPackage/build + + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Apply the Teams app manifest to an existing Teams app in + # Teams Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Extend your Teams app to Outlook and the Microsoft 365 app + - uses: teamsApp/extendToM365 + with: + # Relative path to the build app package. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + titleId: M365_TITLE_ID + appId: M365_APP_ID + +deploy: + # Install development tool(s) + - uses: devTool/install + with: + func: + version: ~4.0.5530 + symlinkDir: ./devTools/func + # Write the information of installed development tool(s) into environment + # file for the specified environment variable(s). + writeToEnvironmentFile: + funcPath: FUNC_PATH + + # Run npm command + - uses: cli/runNpmCommand + name: install dependencies + with: + args: install --no-audit diff --git a/samples/da-SnowWizard/SnowWizard/teamsapp.yml b/samples/da-SnowWizard/SnowWizard/teamsapp.yml new file mode 100644 index 0000000..37888bf --- /dev/null +++ b/samples/da-SnowWizard/SnowWizard/teamsapp.yml @@ -0,0 +1,136 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.7/yaml.schema.json +# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file +# Visit https://aka.ms/teamsfx-actions for details on actions +version: v1.7 + +environmentFolderPath: ./env + +# Triggered when 'teamsapp provision' is executed +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: Snow Wizard${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + - uses: arm/deploy # Deploy given ARM templates parallelly. + with: + # AZURE_SUBSCRIPTION_ID is a built-in environment variable, + # if its value is empty, TeamsFx will prompt you to select a subscription. + # Referencing other environment variables with empty values + # will skip the subscription selection prompt. + subscriptionId: ${{AZURE_SUBSCRIPTION_ID}} + # AZURE_RESOURCE_GROUP_NAME is a built-in environment variable, + # if its value is empty, TeamsFx will prompt you to select or create one + # resource group. + # Referencing other environment variables with empty values + # will skip the resource group selection prompt. + resourceGroupName: ${{AZURE_RESOURCE_GROUP_NAME}} + templates: + - path: ./infra/azure.bicep # Relative path to this file + # Relative path to this yaml file. + # Placeholders will be replaced with corresponding environment + # variable before ARM deployment. + parameters: ./infra/azure.parameters.json + # Required when deploying ARM template + deploymentName: Create-resources-for-sme + # Teams Toolkit will download this bicep CLI version from github for you, + # will use bicep CLI in PATH if you remove this config. + bicepCliVersion: v0.9.1 + + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputFolder: ./appPackage/build + + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Apply the Teams app manifest to an existing Teams app in + # Teams Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Extend your Teams app to Outlook and the Microsoft 365 app + - uses: teamsApp/extendToM365 + with: + # Relative path to the build app package. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + titleId: M365_TITLE_ID + appId: M365_APP_ID + +# Triggered when 'teamsapp deploy' is executed +deploy: + # Run npm command + - uses: cli/runNpmCommand + name: install dependencies + with: + args: install + + - uses: cli/runNpmCommand + name: build app + with: + args: run build --if-present + + # Deploy your application to Azure Functions using the zip deploy feature. + # For additional details, see at https://aka.ms/zip-deploy-to-azure-functions + - uses: azureFunctions/zipDeploy + with: + # deploy base folder + artifactFolder: . + # Ignore file location, leave blank will ignore nothing + ignoreFile: .funcignore + # The resource id of the cloud resource to be deployed to. + # This key will be generated by arm/deploy action automatically. + # You can replace it with your existing Azure Resource id + # or add it to your environment variable file. + resourceId: ${{API_FUNCTION_RESOURCE_ID}} + +# Triggered when 'teamsapp publish' is executed +publish: + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputFolder: ./appPackage/build + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Apply the Teams app manifest to an existing Teams app in + # Teams Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Publish the app to + # Teams Admin Center (https://admin.teams.microsoft.com/policies/manage-apps) + # for review and approval + - uses: teamsApp/publishAppPackage + with: + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + publishedAppId: TEAMS_APP_PUBLISHED_APP_ID +projectId: 001a2152-2c28-4608-b167-7209accdfc03 diff --git a/samples/da-SnowWizard/SnowWizard/tsconfig.json b/samples/da-SnowWizard/SnowWizard/tsconfig.json new file mode 100644 index 0000000..a8d6956 --- /dev/null +++ b/samples/da-SnowWizard/SnowWizard/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "outDir": "dist", + "rootDir": ".", + "sourceMap": true, + "strict": false, + "resolveJsonModule": true, + "esModuleInterop": true, + "typeRoots": ["./node_modules/@types"] + } +} \ No newline at end of file diff --git a/samples/da-SnowWizard/assets/2024-10-11_16-29.png b/samples/da-SnowWizard/assets/2024-10-11_16-29.png new file mode 100644 index 0000000..ae0a2f4 Binary files /dev/null and b/samples/da-SnowWizard/assets/2024-10-11_16-29.png differ diff --git a/samples/da-SnowWizard/assets/2024-10-11_16-39.png b/samples/da-SnowWizard/assets/2024-10-11_16-39.png new file mode 100644 index 0000000..ee26996 Binary files /dev/null and b/samples/da-SnowWizard/assets/2024-10-11_16-39.png differ diff --git a/samples/da-SnowWizard/assets/2024-10-11_16-40.png b/samples/da-SnowWizard/assets/2024-10-11_16-40.png new file mode 100644 index 0000000..a3e85a0 Binary files /dev/null and b/samples/da-SnowWizard/assets/2024-10-11_16-40.png differ diff --git a/samples/da-SnowWizard/assets/sample.json b/samples/da-SnowWizard/assets/sample.json new file mode 100644 index 0000000..fb2ac6a --- /dev/null +++ b/samples/da-SnowWizard/assets/sample.json @@ -0,0 +1,70 @@ +[ + { + "name": "pnp-copilot-pro-dev-snow-wizard", + "source": "pnp", + "title": "Copilot Snow Wizard", + "shortDescription": "M365 Declarative Copilot that interfaces with ServiceNow to list and create incidents", + "url": "https://github.com/pnp/copilot-pro-dev-samples/tree/main/samples/da-SnowWizard", + "downloadUrl": "https://pnp.github.io/download-partial/?url=https://github.com/pnp/copilot-pro-dev-samples/tree/main/samples/da-SnowWizard", + "longDescription": [ + "This project demonstrates a sample implementation of an M365 Declarative Copilot that interfaces with ServiceNow to list and create incidents." + ], + "creationDateTime": "2024-10-11", + "updateDateTime": "2024-10-11", + "products": [ + "Microsoft 365 Copilot" + ], + "metadata": [ + { + "key": "PLATFORM", + "value": "Node.js" + }, + { + "key": "LANGUAGE", + "value": "TypeScript" + }, + { + "key": "API-PLUGIN", + "value": "Yes" + }, + { + "key": "GRAPH-CONNECTOR", + "value": "No" + } + ], + "thumbnails": [ + { + "type": "image", + "order": 100, + "url": "https://github.com/pnp/copilot-pro-dev-samples/raw/main/samples/da-SnowWizard/assets/2024-10-11_16-39.png", + "alt": "Declarative agent doing x" + }, + { + "type": "image", + "order": 100, + "url": "https://github.com/pnp/copilot-pro-dev-samples/raw/main/samples/da-SnowWizard/assets/2024-10-11_16-40.png", + "alt": "Declarative agent doing x" + }, + + ], + "authors": [ + { + "gitHubAccount": "cristianoag", + "pictureUrl": "https://github.com/cristianoag.png", + "name": "Cristiano Goncalves" + }, + { + "gitHubAccount": "luishdemetrio", + "pictureUrl": "https://github.com/luishdemetrio.png", + "name": "Luis Demetrio" + } + ], + "references": [ + { + "name": "Microsoft 365 Copilot extensibility", + "description": "Learn more about what Microsoft 365 Copilot and how you can extend it.", + "url": "https://learn.microsoft.com/microsoft-365-copilot/extensibility/" + } + ] + } + ] \ No newline at end of file