├── .github └── workflows │ └── auto_add_to_project.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── session-delivery-resources ├── README.md ├── custom-engine-agent │ ├── data │ │ ├── Ajlal Nueimat.pdf │ │ ├── Alexis Bourgeois.pdf │ │ ├── Anna Ostrowska.pdf │ │ ├── Anthony Ivanov.pdf │ │ ├── Archie Boyle.pdf │ │ ├── Ariane Meyer.pdf │ │ ├── Arnel Cruz.pdf │ │ ├── Bao Guo.pdf │ │ ├── Brita Tamm.pdf │ │ ├── Carme Fontbernat.pdf │ │ ├── Dabir Al.pdf │ │ ├── Dai Nakamura.pdf │ │ ├── Enric Argilaguet.pdf │ │ ├── Fatma Yilmaz.pdf │ │ ├── Gökçe Aslan.pdf │ │ ├── Hacer Çetin.pdf │ │ ├── Hamed Bettar.pdf │ │ ├── Hanno Simon.pdf │ │ ├── Irena Jaworska.pdf │ │ ├── Isaac Talbot.pdf │ │ ├── Marina Rodríguez.pdf │ │ ├── Marites Reyes.pdf │ │ ├── Marta Guanyavents.pdf │ │ ├── Mia Zimmermann.pdf │ │ ├── Ni Kang.pdf │ │ ├── Nihad Samaha.pdf │ │ ├── Pedro Armijo.pdf │ │ ├── Qi Shi.pdf │ │ ├── Qing Cao.pdf │ │ ├── Renata Hall.pdf │ │ ├── Saara Teinemaa.pdf │ │ ├── Sara Folgueroles.pdf │ │ ├── Shiori Sakamoto.pdf │ │ ├── Yueying Ren.pdf │ │ └── Yuu Shibata.pdf │ └── readme.md └── declarative-agent │ ├── README.md │ └── assets │ ├── copilot-chat-screen.png │ ├── explain-comparison.png │ ├── recommend-product.png │ ├── tell-me-about-contoso-quad.png │ └── working-trey.png └── src ├── README.md ├── custom-engine-agent ├── .gitignore ├── .vscode │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── .webappignore ├── README.md ├── aad.manifest.json ├── adapter.ts ├── app │ ├── actions.ts │ ├── app.ts │ └── card.ts ├── appPackage │ ├── color.png │ ├── manifest.json │ └── outline.png ├── build │ └── aad.manifest.local.json ├── config.ts ├── env │ ├── .env.dev │ ├── .env.local.sample │ ├── .env.local.user.sample │ └── .env.testtool ├── index.ts ├── infra │ ├── azure.bicep │ ├── azure.parameters.json │ └── botRegistration │ │ ├── azurebot.bicep │ │ └── readme.md ├── package-lock.json ├── package.json ├── prompts │ ├── chat │ │ ├── config.json │ │ └── skprompt.txt │ └── monologue │ │ ├── actions.json │ │ ├── config.json │ │ └── skprompt.txt ├── public │ ├── auth-end.html │ └── auth-start.html ├── src │ ├── adapter.ts │ ├── app │ │ ├── actions.ts │ │ ├── app.ts │ │ └── card.ts │ ├── config.ts │ ├── index.ts │ ├── prompts │ │ ├── chat │ │ │ ├── config.json │ │ │ └── skprompt.txt │ │ └── monologue │ │ │ ├── actions.json │ │ │ ├── config.json │ │ │ └── skprompt.txt │ └── public │ │ ├── auth-end.html │ │ └── auth-start.html ├── teamsapp.local.yml ├── teamsapp.testtool.yml ├── teamsapp.yml ├── tsconfig.json └── web.config └── declarative-agent ├── .funcignore ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── README.md ├── TreyResearch API.postman_collection.json ├── appPackage ├── TreyResearch-blue-192.png ├── TreyResearch-blue-32.png ├── extra-icons │ ├── TreyResearch-gold-192.png │ ├── TreyResearch-gold-32.png │ ├── TreyResearch-green-192.png │ ├── TreyResearch-green-32.png │ ├── TreyResearch-red-192.png │ ├── TreyResearch-red-32.png │ ├── color.png │ └── outline.png ├── manifest.json ├── trey-declarative-agent.json ├── trey-definition.json └── trey-plugin.json ├── env ├── .env.dev ├── .env.local.sample └── .env.local.user.sample ├── host.json ├── http └── treyResearchAPI.http ├── infra ├── azure.bicep └── azure.parameters.json ├── local.settings.json ├── package-lock.json ├── package.json ├── sampleDocs ├── Bellows College MSA.docx ├── Bellows College NDA.docx ├── Bellows College SOW - Network security review.docx ├── My Hours.xlsx ├── Woodgrove MSA.docx ├── Woodgrove NDA.docx └── Woodgrove SOW - Financial data plugin for Microsoft Copilot.docx ├── scripts ├── db-setup.js └── db │ ├── Assignment.json │ ├── Consultant.json │ └── Project.json ├── src ├── functions │ ├── consultants.ts │ ├── me.ts │ └── projects.ts ├── model │ ├── apiModel.ts │ ├── baseModel.ts │ └── dbModel.ts └── services │ ├── AssignmentDbService.ts │ ├── ConsultantApiService.ts │ ├── ConsultantDbService.ts │ ├── DbService.ts │ ├── IdentityService.ts │ ├── ProjectApiService.ts │ ├── ProjectDbService.ts │ └── Utilities.ts ├── teamsapp.local.yml ├── teamsapp.yml └── tsconfig.json /.github/workflows/auto_add_to_project.yml: -------------------------------------------------------------------------------- 1 | name: Add new issues to AI Tour GH Project 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | 8 | jobs: 9 | add-to-project: 10 | name: Add issue to project 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/add-to-project@v1.0.2 14 | with: 15 | project-url: ${{ secrets.GH_PROJECT_URL }} 16 | github-token: ${{ secrets.ADD_TO_PROJECT }} 17 | label_issues: 18 | runs-on: ubuntu-latest 19 | permissions: 20 | issues: write 21 | steps: 22 | - run: gh issue edit "$NUMBER" --add-label "$LABELS" 23 | env: 24 | GH_TOKEN: ${{ secrets.ADD_TO_PROJECT }} 25 | GH_REPO: ${{ github.repository }} 26 | NUMBER: ${{ github.event.issue.number }} 27 | LABELS: Extensibility 28 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 9 | # Developer's Guide to Customizing Microsoft Copilot 10 | 11 | ![Developer's Guide to Customizing Microsoft Copilot](https://github.com/user-attachments/assets/fcec3359-83e7-4ec7-849d-079cd677fcfa) 12 | 13 | This repo is a companion to this session at Microsoft AI Tour, a worldwide tour of events. 14 | 15 | ## Session Description 16 | 17 | This session will introduce the components of the Microsoft Copilot stack and explain the different options of extensibility and integration that you can enable with Microsoft Copilot. The session contains architecture and code deepdives that help showcase the different extensibility pathways. 18 | 19 | ## Learning Outcomes 20 | You will learn how to integrate your systems, workflows, and applications directly with Microsoft Copilot, maximizing Copilot's potential through different types of copilot extensions. You will learn which type of extensibility and development pathway is the right one for you and see these extensions be built live during the breakout. 21 | 22 | ## Technology Used 23 | 24 | - Declarative agents 25 | - Plugins 26 | - Custom engine agents 27 | - Teams AI library 28 | - Teams Toolkit 29 | - Visual Studio Code 30 | - Microsoft 365 Copilot 31 | - Azure OpenAI 32 | - Azure AI Search 33 | 34 | ## Additional Resources and Continued Learning 35 | 36 | If you are presenting these slides, you can find additional resources, including the slides for the presentation to deliver this session [here](/session-delivery-resources/README.md). 37 | 38 | | Resources | Links | Description | 39 | |:-------------------|:----------------------------------|:-------------------| 40 | | Copilot Developer Camp | [Link](https://aka.ms/copilotdevcamp) | Learn more about Copilot Extensibility with hands on with Labs and Samples| 41 | | Microsoft 365 Copilot Extensibility Learn Paths | [Link](https://aka.ms/extend-microsoft365-copilot)| Explore Microsoft 365 Copilot Extensibility with Learn paths| 42 | 43 | 44 | ## Content Owners 45 | 46 | 47 | 48 | 49 | 50 | 56 | 62 | 68 | 74 |
51 | Ayca Bas
52 | Ayca Bas 53 |

54 | 📢 55 |
57 | Garry Trinder
58 | Garry Trinder 59 |

60 | 📢 61 |
63 | Rabia Williams
64 | Rabia Williams 65 |

66 | 📢 67 |
69 | Barnam Bora
70 | Barnam Bora 71 |

72 | 📢 73 |
75 | 76 | 77 | 78 | ## Responsible AI 79 | 80 | Microsoft is committed to helping our customers use our AI products responsibly, sharing our learnings, and building trust-based partnerships through tools like Transparency Notes and Impact Assessments. Many of these resources can be found at [https://aka.ms/RAI](https://aka.ms/RAI). 81 | Microsoft’s approach to responsible AI is grounded in our AI principles of fairness, reliability and safety, privacy and security, inclusiveness, transparency, and accountability. 82 | 83 | Large-scale natural language, image, and speech models - like the ones used in this sample - can potentially behave in ways that are unfair, unreliable, or offensive, in turn causing harms. Please consult the [Azure OpenAI service Transparency note](https://learn.microsoft.com/legal/cognitive-services/openai/transparency-note?tabs=text) to be informed about risks and limitations. 84 | 85 | The recommended approach to mitigating these risks is to include a safety system in your architecture that can detect and prevent harmful behavior. [Azure AI Content Safety](https://learn.microsoft.com/azure/ai-services/content-safety/overview) provides an independent layer of protection, able to detect harmful user-generated and AI-generated content in applications and services. Azure AI Content Safety includes text and image APIs that allow you to detect material that is harmful. We also have an interactive Content Safety Studio that allows you to view, explore and try out sample code for detecting harmful content across different modalities. The following [quickstart documentation](https://learn.microsoft.com/azure/ai-services/content-safety/quickstart-text?tabs=visual-studio%2Clinux&pivots=programming-language-rest) guides you through making requests to the service. 86 | 87 | Another aspect to take into account is the overall application performance. With multi-modal and multi-models applications, we consider performance to mean that the system performs as you and your users expect, including not generating harmful outputs. It's important to assess the performance of your overall application using [generation quality and risk and safety metrics](https://learn.microsoft.com/azure/ai-studio/concepts/evaluation-metrics-built-in). 88 | 89 | You can evaluate your AI application in your development environment using the [prompt flow SDK](https://microsoft.github.io/promptflow/index.html). Given either a test dataset or a target, your generative AI application generations are quantitatively measured with built-in evaluators or custom evaluators of your choice. To get started with the prompt flow sdk to evaluate your system, you can follow the [quickstart guide](https://learn.microsoft.com/azure/ai-studio/how-to/develop/flow-evaluate-sdk). Once you execute an evaluation run, you can [visualize the results in Azure AI Studio](https://learn.microsoft.com/azure/ai-studio/how-to/evaluate-flow-results). 90 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # TODO: The maintainer of this repo has not yet edited this file 2 | 3 | **REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project? 4 | 5 | - **No CSS support:** Fill out this template with information about how to file issues and get help. 6 | - **Yes CSS support:** Fill out an intake form at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). CSS will work with/help you to determine next steps. 7 | - **Not sure?** Fill out an intake as though the answer were "Yes". CSS will help you decide. 8 | 9 | *Then remove this first heading from this SUPPORT.MD file before publishing your repo.* 10 | 11 | # Support 12 | 13 | ## How to file issues and get help 14 | 15 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 16 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 17 | feature request as a new Issue. 18 | 19 | For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE 20 | FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER 21 | CHANNEL. WHERE WILL YOU HELP PEOPLE?**. 22 | 23 | ## Microsoft Support Policy 24 | 25 | Support for this **PROJECT or PRODUCT** is limited to the resources listed above. 26 | -------------------------------------------------------------------------------- /session-delivery-resources/README.md: -------------------------------------------------------------------------------- 1 | ## How To Use 2 | 3 | The following resources are intended for a presenter to learn and deliver the session. 4 | 5 | Welcome, 6 | 7 | We're glad you are here and look forward to your delivery of this amazing content. As an experienced presenter, we know you know HOW to present so this guide will focus on WHAT you need to present. It will provide you a full run-through of the presentation created by the presentation design team. 8 | 9 | Along with the video of the presentation, this document will link to all the assets you need to successfully present including PowerPoint slides and demo instructions & 10 | code. 11 | 12 | 1. Read document in its entirety. 13 | 2. Watch the video presentation 14 | 3. Ask questions of the Lead Presenter 15 | 16 | ## File Summary 17 | 18 | | Resources | Links | Description | 19 | |-------------------|----------------------------------|-------------------| 20 | | PowerPoint | [Presentation](https://aka.ms/AAtl1af) | Slides | 21 | | PPT Recording | [Session Recording](https://aka.ms/AAtkk28) | Video Recording of the PowerPoint slides with no audio | 22 | | Demo - Custom Engine Agent 1 & 2| [Instructions](custom-engine-agent) & [Video 1](https://aka.ms/AAtl1ak) and [Video 2](https://aka.ms/AAtl1ai) | Demo instructions and videos | 23 | | Demo - Declarative Agent | [Instructions](declarative-agent) & [Video](https://aka.ms/AAtkk29) | Demo instructions and videos | 24 | 25 | 26 | -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Ajlal Nueimat.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Ajlal Nueimat.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Alexis Bourgeois.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Alexis Bourgeois.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Anna Ostrowska.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Anna Ostrowska.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Anthony Ivanov.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Anthony Ivanov.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Archie Boyle.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Archie Boyle.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Ariane Meyer.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Ariane Meyer.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Arnel Cruz.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Arnel Cruz.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Bao Guo.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Bao Guo.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Brita Tamm.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Brita Tamm.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Carme Fontbernat.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Carme Fontbernat.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Dabir Al.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Dabir Al.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Dai Nakamura.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Dai Nakamura.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Enric Argilaguet.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Enric Argilaguet.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Fatma Yilmaz.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Fatma Yilmaz.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Gökçe Aslan.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Gökçe Aslan.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Hacer Çetin.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Hacer Çetin.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Hamed Bettar.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Hamed Bettar.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Hanno Simon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Hanno Simon.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Irena Jaworska.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Irena Jaworska.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Isaac Talbot.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Isaac Talbot.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Marina Rodríguez.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Marina Rodríguez.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Marites Reyes.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Marites Reyes.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Marta Guanyavents.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Marta Guanyavents.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Mia Zimmermann.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Mia Zimmermann.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Ni Kang.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Ni Kang.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Nihad Samaha.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Nihad Samaha.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Pedro Armijo.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Pedro Armijo.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Qi Shi.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Qi Shi.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Qing Cao.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Qing Cao.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Renata Hall.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Renata Hall.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Saara Teinemaa.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Saara Teinemaa.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Sara Folgueroles.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Sara Folgueroles.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Shiori Sakamoto.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Shiori Sakamoto.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Yueying Ren.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Yueying Ren.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/data/Yuu Shibata.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/custom-engine-agent/data/Yuu Shibata.pdf -------------------------------------------------------------------------------- /session-delivery-resources/custom-engine-agent/readme.md: -------------------------------------------------------------------------------- 1 | # Instructions for Custom engine copilots with​ Teams AI Library and VSCode 2 | 3 | This demo showcases how to build a custom engine copilot with Retrieval-Augmented Generation for sophisticated question answering using Teams AI library and Teams Toolkit. 4 | 5 | Video instructions are available [here](https://aka.ms/AArxxcf). 6 | 7 | ## Pre-requisites 8 | 9 | - [Node.js](https://nodejs.org/), supported versions: 16, 18 10 | - [Teams Toolkit for Visual Studio Code](https://aka.ms/teams-toolkit) version 5.10.1 11 | - Prepare your own [Azure OpenAI](https://aka.ms/oai/access), [Azure AI Search](https://azure.microsoft.com/en-us/products/ai-services/ai-search) and [storage account](https://learn.microsoft.com/en-us/azure/storage/common/storage-account-create) resources. 12 | - A Microsoft 365 Account with a permission to **Upload custom apps** to Teams. 13 | 14 | ## Get started with Azure OpenAI 15 | 16 | 1. Start your demo in Azure OpenAI resource and navigate to Azure AI Foundry. 17 | 2. Create a deployment model, From the **Deployments** tab, select **Create a new deployment**. Fill out the following details and select **Create**: 18 | - **Deployment name:** Recommended to use the same name with the selected deployment model, such as `gpt-4`. 19 | - **Select a model:** Select `gpt-4` or higher model. 20 | - **Model version:** Auto update to default. 21 | - **Deployment type:** Standard. 22 | - **Content Filter:** Default. 23 | 3. Create an embeddings deployment model, select **Create a new deployment**. Fill out the following details and select Create: 24 | - **Select a model:** `text-embedding-ada-002`. 25 | - **Model version:** Default. 26 | - **Deployment type:** Standard. 27 | - **Deployment name:** Choose a memorable name, such as text-embeddings 28 | - **Content Filter:** Default. 29 | 4. Once your models are successfully created, you can navigate to **Chat**. In the **Setup** section, select **Add your data** tab and then **Add a data source**. 30 | 5. Select **Upload files (preview)**, then fill the details as the following and select **Next**: 31 | - **Subscription:** Select the subscription you created your Azure resources. 32 | - **Select Azure Blob storage resource:** Select your storage resource. (You'll see a message *Azure OpenAI needs your permission to access this resource*, select **Turn on CORS**.) 33 | - **Select Azure AI Search resource:** Select your Azure AI Search resournce. 34 | - **Enter the index name:** Index name, such as `resumes`; make note of this. 35 | - Check the box for *Add vector search to this search resource* and select your embedding model. Then select **Next**. 36 | - Select **Browse for a file** and select the pdf documents from the [data](./data) folder. Then, select **Upload files** and **Next**. 37 | - Select Search type as `vector` and chunk size as `1024(Default)`, then **Next**. 38 | - Select `API Key` as Azure resource authentication type, then **Next**. 39 | 5. Once your data ingestion is completed, use Chat playground to ask questions about your data such as: 40 | - "Suggest me .NET developers who can speak Spanish" 41 | - "Suggest me python developers in London Area" 42 | 43 | ## Bring your data to Teams 44 | 45 | 1. Clone this github repo, navigate to [custom-engine-agent](../../src/custom-engine-agent/) folder and open it in Visual Studio Code. 46 | 2. In the project folder, select `env` folder and rename: 47 | - **.env.local.sample** file to **.env.local** 48 | - **.env.local.user.sample** file to **env.local.user** 49 | 3. Open `env.local.user` file and fill the values as shown below and save the file: 50 | 51 | ```json 52 | SECRET_BOT_PASSWORD= 53 | SECRET_AZURE_OPENAI_API_KEY='' 54 | AZURE_OPENAI_ENDPOINT='' 55 | AZURE_OPENAI_DEPLOYMENT_NAME='' 56 | AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME='' 57 | SECRET_AZURE_SEARCH_KEY='' 58 | AZURE_SEARCH_ENDPOINT='' 59 | INDEX_NAME='' 60 | TEAMS_APP_UPDATE_TIME= 61 | SECRET_AAD_APP_CLIENT_SECRET= 62 | HR_EMAIL= 63 | ``` 64 | 65 | Select Teams Toolkit on Visual Studio Code navigation bar, login with Microsoft 365 Account. 66 | 67 | >Make sure that sideloading is enabled for your account. If Sideloading is not enabled yet: 68 | > 69 | >1️⃣ Navigate to https://admin.microsoft.com/, which is the Microsoft 365 Admin Center. 70 | > 71 | >2️⃣ In the left panel of the admin center, select Show all to open up the entire navigation. When the panel opens, select Teams to >open the Microsoft Teams admin center. 72 | > 73 | >3️⃣ In the left of the Microsoft Teams admin center, open the Teams apps accordion. Select Setup Policies, you will see a list of >App setup policies. Then, select the Global (Org-wide default) policy. 74 | > 75 | >4️⃣ Ensure the first switch, Upload custom apps is turned On. 76 | 77 | Let's test the demo on Teams. Start debugging your app by selecting **Run and Debug** tab on Visual Studio Code and Debug in Teams (Edge) or Debug in Teams (Chrome). Microsoft Teams will pop up on your browser. Once your app details show up on Teams, select Add and start chatting with your app. 78 | 79 | >Tip: Testing this exercise locally 80 | > 81 | >Make sure to test and debug this exercise on Teams locally, as some of the Teams AI library capabilities you've implemented in >your app so far won't smoothly work in the Teams App Test Tool. 82 | 83 | Ensure your questions are related to your dataset. Go through pdf documents in the resumes folder to understand more about your data. Challenge your custom engine agent by combining requirements and asking complex questions! Some suggestions would be: 84 | 85 | Can you suggest a candidate who is suitable for spanish speaking role that requires at least 2 years of .NET experience? 86 | Who are the other good candidates? 87 | Who would be suitable for a position that requires 5+ python development experience? 88 | Can you suggest any candidates for a senior developer position with 7+ year experience that requires Japanese speaking? -------------------------------------------------------------------------------- /session-delivery-resources/declarative-agent/README.md: -------------------------------------------------------------------------------- 1 | # Instructions for building declarative agents with Teams Toolkit for Visual Studio Code 2 | 3 | This document showcases how to run the declarative agent, Trey Genie which demonstrates an agent which is grounded in the API plugin as well as in specific SharePoint files. 4 | 5 | The declarative agent contains instructions, a plugin (skills) to call real time API and uses knowledge from documents stored in SharePoint Online as grounding data. 6 | 7 | ## Prerequisites 8 | 9 | - [Node.js 18](https://nodejs.org/) 10 | - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit), v5.10.0 and higher 11 | - Microsoft 365 tenant that is [prepared for testing](https://learn.microsoft.com/%20%20microsoftteams/platform/m365-apps/prerequisites#prepare-a-developer-tenant-for-testing) and has Microsoft 365 Copilot enabled 12 | - User account that has a [Microsoft 365 Copilot license](https://learn.microsoft.com/microsoft-365-copilot/extensibility/prerequisites#prerequisites) 13 | 14 | 15 | ## Set up SharePoint Site 16 | 17 | - Create SharePoint Online site Teams Site with the name `Trey Research legal` 18 | - In it's `Documents` folder upload all the files from [sampleDocs](../../src/declarative-agent/sampleDocs/) folder in this repo. 19 | 20 | ## Set up Trey Genie locally 21 | - Clone this github repo, navigate to folder **src/declarative-agent/** and open it in Visual Studio Code. 22 | - In the folder, select **env** folder and rename: 23 | **.env.local.sample** file to **.env.local** 24 | **.env.local.user.sample** file to **env.local.user** 25 | - Open **env.local** file and update the value of `SHAREPOINT_DOCS_URL` with you SharePoint Online site's absolute URL, where you uploaded the sample documents in the previous step 26 | 27 | ## Run Trey Genie locally in Teams 28 | 29 | Select Teams Toolkit on Visual Studio Code navigation bar, login with Microsoft 365 Account. 30 | 31 | >Make sure that sideloading is enabled for your account. If Sideloading is not enabled yet: 32 | > 33 | >1️⃣ Navigate to https://admin.microsoft.com/, which is the Microsoft 365 Admin Center. 34 | > 35 | >2️⃣ In the left panel of the admin center, select Show all to open up the entire navigation. When the panel opens, select Teams to >open the Microsoft Teams admin center. 36 | > 37 | >3️⃣ In the left of the Microsoft Teams admin center, open the Teams apps accordion. Select Setup Policies, you will see a list of >App setup policies. Then, select the Global (Org-wide default) policy. 38 | > 39 | >4️⃣ Ensure the first switch, Upload custom apps is turned On. 40 | 41 | Start debugging your app by selecting **Run and Debug** tab on Visual Studio Code and Debug in Teams (Edge) or Debug in Teams (Chrome). Or select F5. 42 | 43 | Microsoft Teams will pop up on your browser. 44 | Open the Copilot chat 1️⃣ and the right flyout 2️⃣ to show your previous chats and declarative agents and select the Trey Genie Local copilot 3️⃣ . 45 | 46 | ![chat screen in Teams](../declarative-agent/assets/copilot-chat-screen.png) 47 | 48 | Try one of the conversation starters like `Find consultants with TypeScript skills`. 49 | 50 | ![Trey genie working](../declarative-agent/assets/working-trey.png) 51 | 52 | ### Prompts that work in the completed solution 53 | 54 | * what trey projects am i assigned to? 55 | (NOTE: The logged in user is assumed to be consultant "Avery Howard". As this sample does not have Auth) 56 | * what trey projects is domi working on? 57 | * do we have any trey consultants with azure certifications? 58 | * what trey projects are we doing for relecloud? 59 | * which trey consultants are working with woodgrove bank? 60 | * in trey research, how many hours has avery delivered this month? 61 | * please find a trey consultant with python skills who is available immediately 62 | * are any trey research consultants available who are AWS certified? (multi-parameter!) 63 | * does trey research have any architects with javascript skills? (multi-parameter!) 64 | * what trey research designers are working at woodgrove bank? (multi-parameter!) 65 | * please charge 10 hours to woodgrove bank in trey research (POST request) 66 | * please add sanjay to the contoso project for trey research (POST request with easy to forget entities, hoping to prompt the user; for now they are defaulted) 67 | 68 | -------------------------------------------------------------------------------- /session-delivery-resources/declarative-agent/assets/copilot-chat-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/declarative-agent/assets/copilot-chat-screen.png -------------------------------------------------------------------------------- /session-delivery-resources/declarative-agent/assets/explain-comparison.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/declarative-agent/assets/explain-comparison.png -------------------------------------------------------------------------------- /session-delivery-resources/declarative-agent/assets/recommend-product.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/declarative-agent/assets/recommend-product.png -------------------------------------------------------------------------------- /session-delivery-resources/declarative-agent/assets/tell-me-about-contoso-quad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/declarative-agent/assets/tell-me-about-contoso-quad.png -------------------------------------------------------------------------------- /session-delivery-resources/declarative-agent/assets/working-trey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/session-delivery-resources/declarative-agent/assets/working-trey.png -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # Source code 2 | 3 | This folder contains the source code for this session demos. 4 | -------------------------------------------------------------------------------- /src/custom-engine-agent/.gitignore: -------------------------------------------------------------------------------- 1 | # TeamsFx files 2 | env/.env.*.user 3 | env/.env.local 4 | .localConfigs 5 | .localConfigs.testTool 6 | .localConfigs 7 | .notification.localstore.json 8 | .notification.testtoolstore.json 9 | appPackage/build 10 | 11 | # dependencies 12 | node_modules/ 13 | 14 | # misc 15 | .env 16 | .deployment 17 | .DS_Store 18 | 19 | # build 20 | lib/ 21 | 22 | # Dev tool directories 23 | /devTools/ -------------------------------------------------------------------------------- /src/custom-engine-agent/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "TeamsDevApp.ms-teams-vscode-extension" 4 | ] 5 | } -------------------------------------------------------------------------------- /src/custom-engine-agent/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch Remote (Edge)", 6 | "type": "msedge", 7 | "request": "launch", 8 | "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", 9 | "presentation": { 10 | "group": "3-remote", 11 | "order": 1 12 | }, 13 | "internalConsoleOptions": "neverOpen" 14 | }, 15 | { 16 | "name": "Launch Remote (Chrome)", 17 | "type": "chrome", 18 | "request": "launch", 19 | "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", 20 | "presentation": { 21 | "group": "3-remote", 22 | "order": 2 23 | }, 24 | "internalConsoleOptions": "neverOpen" 25 | }, 26 | { 27 | "name": "Launch App (Edge)", 28 | "type": "msedge", 29 | "request": "launch", 30 | "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", 31 | "cascadeTerminateToConfigurations": [ 32 | "Attach to Local Service" 33 | ], 34 | "presentation": { 35 | "group": "all", 36 | "hidden": true 37 | }, 38 | "internalConsoleOptions": "neverOpen" 39 | }, 40 | { 41 | "name": "Launch App (Chrome)", 42 | "type": "chrome", 43 | "request": "launch", 44 | "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", 45 | "cascadeTerminateToConfigurations": [ 46 | "Attach to Local Service" 47 | ], 48 | "presentation": { 49 | "group": "all", 50 | "hidden": true 51 | }, 52 | "internalConsoleOptions": "neverOpen" 53 | }, 54 | { 55 | "name": "Attach to Local Service", 56 | "type": "node", 57 | "request": "attach", 58 | "port": 9239, 59 | "restart": true, 60 | "presentation": { 61 | "group": "all", 62 | "hidden": true 63 | }, 64 | "internalConsoleOptions": "neverOpen" 65 | } 66 | ], 67 | "compounds": [ 68 | { 69 | "name": "Debug in Teams (Edge)", 70 | "configurations": [ 71 | "Launch App (Edge)", 72 | "Attach to Local Service" 73 | ], 74 | "preLaunchTask": "Start Teams App Locally", 75 | "presentation": { 76 | "group": "2-local", 77 | "order": 1 78 | }, 79 | "stopAll": true 80 | }, 81 | { 82 | "name": "Debug in Teams (Chrome)", 83 | "configurations": [ 84 | "Launch App (Chrome)", 85 | "Attach to Local Service" 86 | ], 87 | "preLaunchTask": "Start Teams App Locally", 88 | "presentation": { 89 | "group": "2-local", 90 | "order": 2 91 | }, 92 | "stopAll": true 93 | }, 94 | { 95 | "name": "Debug in Test Tool", 96 | "configurations": [ 97 | "Attach to Local Service" 98 | ], 99 | "preLaunchTask": "Start Teams App (Test Tool)", 100 | "presentation": { 101 | "group": "1-local", 102 | "order": 1 103 | }, 104 | "stopAll": true 105 | } 106 | ] 107 | } 108 | -------------------------------------------------------------------------------- /src/custom-engine-agent/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "debug.onTaskErrors": "abort", 3 | "json.schemas": [ 4 | { 5 | "fileMatch": [ 6 | "/aad.*.json" 7 | ], 8 | "schema": {} 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /src/custom-engine-agent/.webappignore: -------------------------------------------------------------------------------- 1 | .webappignore 2 | .fx 3 | .deployment 4 | .localConfigs.testTool 5 | .localConfigs 6 | .notification.localstore.json 7 | .notification.testtoolstore.json 8 | .vscode 9 | *.js.map 10 | *.ts.map 11 | *.ts 12 | .git* 13 | .tsbuildinfo 14 | CHANGELOG.md 15 | readme.md 16 | local.settings.json 17 | test 18 | tsconfig.json 19 | .DS_Store 20 | teamsapp.yml 21 | teamsapp.*.yml 22 | /env/ 23 | /node_modules/.bin 24 | /node_modules/ts-node 25 | /node_modules/typescript 26 | /appPackage/ 27 | /infra/ 28 | /devTools/ -------------------------------------------------------------------------------- /src/custom-engine-agent/README.md: -------------------------------------------------------------------------------- 1 | # Get started with the sample 2 | 3 | Visit [session-delivery-resources](../../session-delivery-resources/custom-engine-agent/) to learn more about running this sample locally. 4 | 5 | ## Final application 6 | 7 | This folder contains a solution from the [Copilot Developer Camp labs](https://aka.ms/copilotdevcamp). When completed, the solution is a custom engine agent called "Career Genie" which provides assistant to Human Resources team in Trey Research. Trey Research is a fictitious consulting company that supplies talent in the software and pharmaceuticals industries. The vision for this demo is to show a sophisticated custom engine agent that utilizes custom AI models and orchestrator to meet the unique needs of the Human Resources department such as managing resumes, create new job posts and more. 8 | 9 | ### Prompts that work in the completed solution 10 | 11 | - Hello 12 | - Can you suggest candidates who have experience in .NET? 13 | - Great, add Isaac Talbot in the .NET Developer Candidates list 14 | - Add Anthony Ivanov in the same list with Isaac 15 | - Can you summarize my lists 16 | - Suggest candidates who have experience in Python and are able to speak Spanish 17 | - Nice! Add Sara Folgueroles in the Python Developer Candidates (Spanish speaking) list 18 | - Can you suggest candidates who have 10+ years of experience 19 | - Ok, remove Anthony from the .NET Developer Candidates list 20 | - Add Anthony Ivanov in the Talent list 21 | - Summarize my lists 22 | - Add Pedro Armijo in the same list with Sara 23 | - Summarize my lists 24 | - Send my lists to HR -------------------------------------------------------------------------------- /src/custom-engine-agent/aad.manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "${{AAD_APP_OBJECT_ID}}", 3 | "appId": "${{AAD_APP_CLIENT_ID}}", 4 | "name": "CareerGenieBot-aad", 5 | "accessTokenAcceptedVersion": 2, 6 | "signInAudience": "AzureADMultipleOrgs", 7 | "optionalClaims": { 8 | "idToken": [], 9 | "accessToken": [ 10 | { 11 | "name": "idtyp", 12 | "source": null, 13 | "essential": false, 14 | "additionalProperties": [] 15 | } 16 | ], 17 | "saml2Token": [] 18 | }, 19 | "requiredResourceAccess": [ 20 | { 21 | "resourceAppId": "Microsoft Graph", 22 | "resourceAccess": [ 23 | { 24 | "id": "User.Read", 25 | "type": "Scope" 26 | }, 27 | { 28 | "id": "Mail.Read", 29 | "type": "Scope" 30 | } 31 | ] 32 | } 33 | ], 34 | "oauth2Permissions": [ 35 | { 36 | "adminConsentDescription": "Allows Teams to call the app's web APIs as the current user.", 37 | "adminConsentDisplayName": "Teams can access app's web APIs", 38 | "id": "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}", 39 | "isEnabled": true, 40 | "type": "User", 41 | "userConsentDescription": "Enable Teams to call this app's web APIs with the same rights that you have", 42 | "userConsentDisplayName": "Teams can access app's web APIs and make requests on your behalf", 43 | "value": "access_as_user" 44 | } 45 | ], 46 | "preAuthorizedApplications": [ 47 | { 48 | "appId": "1fec8e78-bce4-4aaf-ab1b-5451cc387264", 49 | "permissionIds": [ 50 | "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" 51 | ] 52 | }, 53 | { 54 | "appId": "5e3ce6c0-2b1f-4285-8d4b-75ee78787346", 55 | "permissionIds": [ 56 | "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" 57 | ] 58 | }, 59 | { 60 | "appId": "d3590ed6-52b3-4102-aeff-aad2292ab01c", 61 | "permissionIds": [ 62 | "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" 63 | ] 64 | }, 65 | { 66 | "appId": "00000002-0000-0ff1-ce00-000000000000", 67 | "permissionIds": [ 68 | "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" 69 | ] 70 | }, 71 | { 72 | "appId": "bc59ab01-8403-45c6-8796-ac3ef710b3e3", 73 | "permissionIds": [ 74 | "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" 75 | ] 76 | }, 77 | { 78 | "appId": "0ec893e0-5785-4de6-99da-4ed124e5296c", 79 | "permissionIds": [ 80 | "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" 81 | ] 82 | }, 83 | { 84 | "appId": "4765445b-32c6-49b0-83e6-1d93765276ca", 85 | "permissionIds": [ 86 | "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" 87 | ] 88 | }, 89 | { 90 | "appId": "4345a7b9-9a63-4910-a426-35363201d503", 91 | "permissionIds": [ 92 | "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" 93 | ] 94 | } 95 | ], 96 | "identifierUris":[ 97 | "api://botid-${{BOT_ID}}" 98 | ], 99 | "replyUrlsWithType":[ 100 | { 101 | "url": "https://${{BOT_DOMAIN}}/auth-end.html", 102 | "type": "Web" 103 | } 104 | ] 105 | } -------------------------------------------------------------------------------- /src/custom-engine-agent/adapter.ts: -------------------------------------------------------------------------------- 1 | // Import required bot services. 2 | // See https://aka.ms/bot-services to learn more about the different parts of a bot. 3 | import { 4 | CloudAdapter, 5 | ConfigurationBotFrameworkAuthentication, 6 | ConfigurationServiceClientCredentialFactory, 7 | } from "botbuilder"; 8 | 9 | // This bot's main dialog. 10 | import config from "./config"; 11 | import { TeamsAdapter } from '@microsoft/teams-ai'; 12 | // const botFrameworkAuthentication = new ConfigurationBotFrameworkAuthentication( 13 | // {}, 14 | // new ConfigurationServiceClientCredentialFactory({ 15 | // MicrosoftAppId: config.botId, 16 | // MicrosoftAppPassword: process.env.BOT_PASSWORD, 17 | // MicrosoftAppType: "MultiTenant", 18 | // }) 19 | // ); 20 | 21 | // Create adapter. 22 | // See https://aka.ms/about-bot-adapter to learn more about how bots work. 23 | const adapter = new TeamsAdapter( 24 | {}, 25 | new ConfigurationServiceClientCredentialFactory({ 26 | MicrosoftAppId: config.botId, 27 | MicrosoftAppPassword: process.env.BOT_PASSWORD, 28 | MicrosoftAppType: 'MultiTenant', 29 | }) 30 | ); 31 | 32 | // Catch-all for errors. 33 | const onTurnErrorHandler = async (context, error) => { 34 | // This check writes out errors to console log .vs. app insights. 35 | // NOTE: In production environment, you should consider logging this to Azure 36 | // application insights. 37 | console.error(`\n [onTurnError] unhandled error: ${error}`); 38 | 39 | // Only send error message for user messages, not for other message types so the bot doesn't spam a channel or chat. 40 | if (context.activity.type === "message") { 41 | // Send a trace activity, which will be displayed in Bot Framework Emulator 42 | await context.sendTraceActivity( 43 | "OnTurnError Trace", 44 | `${error}`, 45 | "https://www.botframework.com/schemas/error", 46 | "TurnError" 47 | ); 48 | 49 | // Send a message to the user 50 | await context.sendActivity("The bot encountered an error or bug."); 51 | await context.sendActivity("To continue to run this bot, please fix the bot source code."); 52 | } 53 | }; 54 | 55 | // Set the onTurnError for the singleton CloudAdapter. 56 | adapter.onTurnError = onTurnErrorHandler; 57 | 58 | export default adapter; 59 | -------------------------------------------------------------------------------- /src/custom-engine-agent/app/actions.ts: -------------------------------------------------------------------------------- 1 | import { getUserDisplayName, ApplicationTurnState } from './app'; 2 | import { Client } from "@microsoft/microsoft-graph-client"; 3 | import config from '../config'; 4 | 5 | function getCandidates(state: ApplicationTurnState, list: string): string[] { 6 | ensureListExists(state, list); 7 | return state.conversation.lists[list]; 8 | } 9 | 10 | function setCandidates(state: ApplicationTurnState, list: string, Candidates: string[]): void { 11 | ensureListExists(state, list); 12 | state.conversation.lists[list] = Candidates ?? []; 13 | } 14 | 15 | function ensureListExists(state: ApplicationTurnState, listName: string): void { 16 | if (typeof state.conversation.lists != 'object') { 17 | state.conversation.lists = {}; 18 | } 19 | 20 | if (!Object.prototype.hasOwnProperty.call(state.conversation.lists, listName)) { 21 | state.conversation.lists[listName] = []; 22 | } 23 | } 24 | 25 | function deleteList(state: ApplicationTurnState, listName: string): void { 26 | if ( 27 | typeof state.conversation.lists == 'object' && 28 | Object.prototype.hasOwnProperty.call(state.conversation.lists, listName) 29 | ) { 30 | delete state.conversation.lists[listName]; 31 | } 32 | } 33 | 34 | async function sendLists(state: ApplicationTurnState, token): Promise { 35 | const email = await createEmailContent(state.conversation.lists, token); 36 | try { 37 | const client = Client.init({ 38 | authProvider: (done) => { 39 | done(null, token); 40 | } 41 | }); 42 | const sendEmail = await client.api('/me/sendMail').post(JSON.stringify(email)); 43 | if (sendEmail.ok) { 44 | return email.message.body.content; 45 | } 46 | else { 47 | console.log(`Error ${sendEmail.status} calling Graph in sendToHR: ${sendEmail.statusText}`); 48 | return 'Error sending email'; 49 | } 50 | } catch (error) { 51 | console.error('Error in sendLists:', error); 52 | throw error; 53 | } 54 | } 55 | 56 | async function createEmailContent(lists, token) { 57 | let emailContent = ''; 58 | for (const listName in lists) { 59 | if (lists.hasOwnProperty(listName)) { 60 | emailContent += `${listName}:\n`; 61 | lists[listName].forEach(candidate => { 62 | emailContent += ` • ${candidate}\n`; 63 | }); 64 | emailContent += '\n'; // Add an extra line between different lists 65 | } 66 | } 67 | 68 | const profileName = await getUserDisplayName(token); 69 | 70 | const email = { 71 | "message": { 72 | "subject": "Request to Schedule Interviews with Shortlisted Candidates", 73 | "body": { 74 | "contentType": "Text", 75 | "content": `Hello HR Team, \nI hope this email finds you well. \n\nCould you please assist in scheduling 1:1 interviews with the following shortlisted candidates? \n\n${emailContent} Please arrange suitable times and send out the calendar invites accordingly. \n\n Best Regards, \n ${profileName}` 76 | }, 77 | "toRecipients": [ 78 | { 79 | "emailAddress": { 80 | "address": `${config.HR_EMAIL}` 81 | } 82 | } 83 | ] 84 | }, 85 | "saveToSentCandidates": "true" 86 | }; 87 | return await email; 88 | } 89 | 90 | export { getCandidates, setCandidates, ensureListExists, deleteList, sendLists }; -------------------------------------------------------------------------------- /src/custom-engine-agent/app/card.ts: -------------------------------------------------------------------------------- 1 | import { AdaptiveCard, Message, Utilities } from '@microsoft/teams-ai'; 2 | /** 3 | * Create an adaptive card from a prompt response. 4 | * @param {Message} response The prompt response to create the card from. 5 | * @returns {AdaptiveCard} The response card. 6 | */ 7 | 8 | //Adaptive card to display the response and citations 9 | export function createResponseCard(response: Message): AdaptiveCard { 10 | const citationCards = response.context?.citations.map((citation, i) => ({ 11 | type: 'Action.ShowCard', 12 | title: `${i+1}`, 13 | card: { 14 | type: 'AdaptiveCard', 15 | body: [ 16 | { 17 | type: 'TextBlock', 18 | text: citation.title, 19 | fontType: 'Default', 20 | weight: 'Bolder' 21 | }, 22 | { 23 | type: 'TextBlock', 24 | text: citation.content, 25 | wrap: true 26 | } 27 | ] 28 | } 29 | })); 30 | 31 | const text = Utilities.formatCitationsResponse(response.content!); 32 | return { 33 | type: 'AdaptiveCard', 34 | body: [ 35 | { 36 | type: 'TextBlock', 37 | text: text, 38 | wrap: true 39 | }, 40 | { 41 | type: 'TextBlock', 42 | text: 'Citations', 43 | wrap: true, 44 | fontType: 'Default', 45 | weight: 'Bolder' 46 | }, 47 | { 48 | type: 'ActionSet', 49 | actions: citationCards 50 | } 51 | ], 52 | $schema: 'http://adaptivecards.io/schemas/adaptive-card.json', 53 | version: '1.5' 54 | }; 55 | } 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/custom-engine-agent/appPackage/color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/src/custom-engine-agent/appPackage/color.png -------------------------------------------------------------------------------- /src/custom-engine-agent/appPackage/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.16/MicrosoftTeams.schema.json", 3 | "manifestVersion": "1.16", 4 | "version": "1.0.0", 5 | "id": "${{TEAMS_APP_ID}}", 6 | "packageName": "com.microsoft.teams.extension", 7 | "developer": { 8 | "name": "Teams App, Inc.", 9 | "websiteUrl": "https://www.example.com", 10 | "privacyUrl": "https://www.example.com/privacy", 11 | "termsOfUseUrl": "https://www.example.com/termofuse" 12 | }, 13 | "icons": { 14 | "color": "color.png", 15 | "outline": "outline.png" 16 | }, 17 | "name": { 18 | "short": "CareerGenie${{APP_NAME_SUFFIX}}", 19 | "full": "full name for CareerGenie" 20 | }, 21 | "description": { 22 | "short": "short description for CareerGenie", 23 | "full": "full description for CareerGenie" 24 | }, 25 | "accentColor": "#FFFFFF", 26 | "bots": [ 27 | { 28 | "botId": "${{BOT_ID}}", 29 | "scopes": ["personal", "team", "groupChat"], 30 | "isNotificationOnly": false, 31 | "supportsCalling": false, 32 | "supportsVideo": false, 33 | "supportsFiles": false 34 | } 35 | ], 36 | "composeExtensions": [], 37 | "configurableTabs": [], 38 | "staticTabs": [], 39 | "permissions": [ 40 | "identity", 41 | "messageTeamMembers" 42 | ], 43 | "validDomains": [ 44 | "${{BOT_DOMAIN}}", 45 | "*.botframework.com" 46 | ], 47 | "webApplicationInfo": { 48 | "id": "${{BOT_ID}}", 49 | "resource": "api://botid-${{BOT_ID}}" 50 | } 51 | } -------------------------------------------------------------------------------- /src/custom-engine-agent/appPackage/outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/src/custom-engine-agent/appPackage/outline.png -------------------------------------------------------------------------------- /src/custom-engine-agent/build/aad.manifest.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "ba8e6ff2-657f-46ac-bed6-8665bf726691", 3 | "appId": "250a006b-c87d-40c0-be4d-be72841a0a2d", 4 | "name": "CareerGenieBot-aad", 5 | "accessTokenAcceptedVersion": 2, 6 | "signInAudience": "AzureADMultipleOrgs", 7 | "optionalClaims": { 8 | "idToken": [], 9 | "accessToken": [ 10 | { 11 | "name": "idtyp", 12 | "source": null, 13 | "essential": false, 14 | "additionalProperties": [] 15 | } 16 | ], 17 | "saml2Token": [] 18 | }, 19 | "requiredResourceAccess": [ 20 | { 21 | "resourceAppId": "00000003-0000-0000-c000-000000000000", 22 | "resourceAccess": [ 23 | { 24 | "id": "e1fe6dd8-ba31-4d61-89e7-88639da4683d", 25 | "type": "Scope" 26 | }, 27 | { 28 | "id": "570282fd-fa5c-430d-a7fd-fc8dc98a9dca", 29 | "type": "Scope" 30 | } 31 | ] 32 | } 33 | ], 34 | "oauth2Permissions": [ 35 | { 36 | "adminConsentDescription": "Allows Teams to call the app's web APIs as the current user.", 37 | "adminConsentDisplayName": "Teams can access app's web APIs", 38 | "id": "d65b865b-d634-43a9-a872-692bebc2b7e9", 39 | "isEnabled": true, 40 | "type": "User", 41 | "userConsentDescription": "Enable Teams to call this app's web APIs with the same rights that you have", 42 | "userConsentDisplayName": "Teams can access app's web APIs and make requests on your behalf", 43 | "value": "access_as_user" 44 | } 45 | ], 46 | "preAuthorizedApplications": [ 47 | { 48 | "appId": "1fec8e78-bce4-4aaf-ab1b-5451cc387264", 49 | "permissionIds": [ 50 | "d65b865b-d634-43a9-a872-692bebc2b7e9" 51 | ] 52 | }, 53 | { 54 | "appId": "5e3ce6c0-2b1f-4285-8d4b-75ee78787346", 55 | "permissionIds": [ 56 | "d65b865b-d634-43a9-a872-692bebc2b7e9" 57 | ] 58 | }, 59 | { 60 | "appId": "d3590ed6-52b3-4102-aeff-aad2292ab01c", 61 | "permissionIds": [ 62 | "d65b865b-d634-43a9-a872-692bebc2b7e9" 63 | ] 64 | }, 65 | { 66 | "appId": "00000002-0000-0ff1-ce00-000000000000", 67 | "permissionIds": [ 68 | "d65b865b-d634-43a9-a872-692bebc2b7e9" 69 | ] 70 | }, 71 | { 72 | "appId": "bc59ab01-8403-45c6-8796-ac3ef710b3e3", 73 | "permissionIds": [ 74 | "d65b865b-d634-43a9-a872-692bebc2b7e9" 75 | ] 76 | }, 77 | { 78 | "appId": "0ec893e0-5785-4de6-99da-4ed124e5296c", 79 | "permissionIds": [ 80 | "d65b865b-d634-43a9-a872-692bebc2b7e9" 81 | ] 82 | }, 83 | { 84 | "appId": "4765445b-32c6-49b0-83e6-1d93765276ca", 85 | "permissionIds": [ 86 | "d65b865b-d634-43a9-a872-692bebc2b7e9" 87 | ] 88 | }, 89 | { 90 | "appId": "4345a7b9-9a63-4910-a426-35363201d503", 91 | "permissionIds": [ 92 | "d65b865b-d634-43a9-a872-692bebc2b7e9" 93 | ] 94 | } 95 | ], 96 | "identifierUris": [ 97 | "api://botid-e0a45687-5765-4e61-9120-24879c12a27e" 98 | ], 99 | "replyUrlsWithType": [ 100 | { 101 | "url": "https://1p3tpxsq-3978.use.devtunnels.ms/auth-end.html", 102 | "type": "Web" 103 | } 104 | ] 105 | } -------------------------------------------------------------------------------- /src/custom-engine-agent/config.ts: -------------------------------------------------------------------------------- 1 | const config = { 2 | botId: process.env.BOT_ID, 3 | botPassword: process.env.BOT_PASSWORD, 4 | azureOpenAIKey: process.env.AZURE_OPENAI_API_KEY, 5 | azureOpenAIEndpoint: process.env.AZURE_OPENAI_ENDPOINT, 6 | azureOpenAIDeploymentName: process.env.AZURE_OPENAI_DEPLOYMENT_NAME, 7 | azureOpenAIEmbeddingDeploymentName: process.env.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME, 8 | azureSearchKey: process.env.AZURE_SEARCH_KEY, 9 | azureSearchEndpoint: process.env.AZURE_SEARCH_ENDPOINT, 10 | indexName: process.env.INDEX_NAME, 11 | aadAppClientId: process.env.AAD_APP_CLIENT_ID, 12 | aadAppClientSecret: process.env.AAD_APP_CLIENT_SECRET, 13 | aadAppOauthAuthorityHost: process.env.AAD_APP_OAUTH_AUTHORITY_HOST, 14 | aadAppTenantId: process.env.AAD_APP_TENANT_ID, 15 | botDomain: process.env.BOT_DOMAIN, 16 | aadAppOauthAuthority: process.env.AAD_APP_OAUTH_AUTHORITY, 17 | HR_EMAIL: process.env.HR_EMAIL, 18 | }; 19 | 20 | export default config; 21 | -------------------------------------------------------------------------------- /src/custom-engine-agent/env/.env.dev: -------------------------------------------------------------------------------- 1 | # This file includes environment variables that will be committed to git by default. 2 | 3 | # Built-in environment variables 4 | TEAMSFX_ENV=dev 5 | APP_NAME_SUFFIX=dev 6 | 7 | # 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. 8 | AZURE_SUBSCRIPTION_ID= 9 | AZURE_RESOURCE_GROUP_NAME= 10 | RESOURCE_SUFFIX= 11 | 12 | # Generated during provision, you can also add your own variables. 13 | BOT_ID= 14 | TEAMS_APP_ID= 15 | BOT_AZURE_APP_SERVICE_RESOURCE_ID= 16 | BOT_DOMAIN= -------------------------------------------------------------------------------- /src/custom-engine-agent/env/.env.local.sample: -------------------------------------------------------------------------------- 1 | # This file includes environment variables that will be committed to git by default. 2 | 3 | # Built-in environment variables 4 | TEAMSFX_ENV=local 5 | APP_NAME_SUFFIX=local 6 | 7 | # 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. 8 | AZURE_SUBSCRIPTION_ID= 9 | AZURE_RESOURCE_GROUP_NAME= 10 | RESOURCE_SUFFIX= 11 | 12 | # Generated during provision, you can also add your own variables. 13 | BOT_ID= 14 | TEAMS_APP_ID= 15 | BOT_AZURE_APP_SERVICE_RESOURCE_ID= 16 | BOT_DOMAIN= 17 | BOT_ENDPOINT= 18 | AAD_APP_CLIENT_ID= 19 | AAD_APP_OBJECT_ID= 20 | AAD_APP_TENANT_ID= 21 | AAD_APP_OAUTH_AUTHORITY= 22 | AAD_APP_OAUTH_AUTHORITY_HOST= 23 | TEAMS_APP_TENANT_ID= 24 | AAD_APP_ACCESS_AS_USER_PERMISSION_ID= -------------------------------------------------------------------------------- /src/custom-engine-agent/env/.env.local.user.sample: -------------------------------------------------------------------------------- 1 | SECRET_BOT_PASSWORD= 2 | SECRET_AZURE_OPENAI_API_KEY='' 3 | AZURE_OPENAI_ENDPOINT='' 4 | AZURE_OPENAI_DEPLOYMENT_NAME='' 5 | AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME='' 6 | SECRET_AZURE_SEARCH_KEY='' 7 | AZURE_SEARCH_ENDPOINT='' 8 | INDEX_NAME='' 9 | TEAMS_APP_UPDATE_TIME= 10 | SECRET_AAD_APP_CLIENT_SECRET= 11 | HR_EMAIL= -------------------------------------------------------------------------------- /src/custom-engine-agent/env/.env.testtool: -------------------------------------------------------------------------------- 1 | # This file includes environment variables that can be committed to git. It's gitignored by default because it represents your local development environment. 2 | 3 | # Built-in environment variables 4 | TEAMSFX_ENV=testtool 5 | 6 | # Environment variables used by test tool 7 | TEAMSAPPTESTER_PORT=56150 8 | TEAMSFX_NOTIFICATION_STORE_FILENAME=.notification.testtoolstore.json -------------------------------------------------------------------------------- /src/custom-engine-agent/index.ts: -------------------------------------------------------------------------------- 1 | // Import required packages 2 | import * as restify from "restify"; 3 | import * as path from 'path'; 4 | // This bot's adapter 5 | import adapter from "./adapter"; 6 | 7 | // This bot's main dialog. 8 | import app from "./app/app"; 9 | 10 | // Create HTTP server. 11 | const server = restify.createServer(); 12 | server.use(restify.plugins.bodyParser()); 13 | 14 | server.listen(process.env.port || process.env.PORT || 3978, () => { 15 | console.log(`\nBot Started, ${server.name} listening to ${server.url}`); 16 | }); 17 | server.get( 18 | '/auth-:name(start|end).html', 19 | restify.plugins.serveStatic({ 20 | directory: path.join(__dirname, 'public') 21 | }) 22 | ); 23 | 24 | // Listen for incoming server requests. 25 | server.post("/api/messages", async (req, res) => { 26 | // Route received a request to adapter for processing 27 | await adapter.process(req, res as any, async (context) => { 28 | // Dispatch to application for routing 29 | await app.run(context); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/custom-engine-agent/infra/azure.bicep: -------------------------------------------------------------------------------- 1 | @maxLength(20) 2 | @minLength(4) 3 | @description('Used to generate names for all resources in this file') 4 | param resourceBaseName string 5 | 6 | @description('Required when create Azure Bot service') 7 | param botAadAppClientId string 8 | 9 | @secure() 10 | @description('Required by Bot Framework package in your bot project') 11 | param botAadAppClientSecret string 12 | 13 | @secure() 14 | param azureOpenAIKey string 15 | 16 | @secure() 17 | param azureOpenAIEndpoint string 18 | 19 | @secure() 20 | param azureOpenAIDeploymentName string 21 | 22 | param webAppSKU string 23 | 24 | @maxLength(42) 25 | param botDisplayName string 26 | 27 | param serverfarmsName string = resourceBaseName 28 | param webAppName string = resourceBaseName 29 | param location string = resourceGroup().location 30 | 31 | // Compute resources for your Web App 32 | resource serverfarm 'Microsoft.Web/serverfarms@2021-02-01' = { 33 | kind: 'app' 34 | location: location 35 | name: serverfarmsName 36 | sku: { 37 | name: webAppSKU 38 | } 39 | } 40 | 41 | // Web App that hosts your bot 42 | resource webApp 'Microsoft.Web/sites@2021-02-01' = { 43 | kind: 'app' 44 | location: location 45 | name: webAppName 46 | properties: { 47 | serverFarmId: serverfarm.id 48 | httpsOnly: true 49 | siteConfig: { 50 | alwaysOn: true 51 | appSettings: [ 52 | { 53 | name: 'WEBSITE_RUN_FROM_PACKAGE' 54 | value: '1' // Run Azure App Service from a package file 55 | } 56 | { 57 | name: 'WEBSITE_NODE_DEFAULT_VERSION' 58 | value: '~18' // Set NodeJS version to 18.x for your site 59 | } 60 | { 61 | name: 'RUNNING_ON_AZURE' 62 | value: '1' 63 | } 64 | { 65 | name: 'BOT_ID' 66 | value: botAadAppClientId 67 | } 68 | { 69 | name: 'BOT_PASSWORD' 70 | value: botAadAppClientSecret 71 | } 72 | { 73 | name: 'AZURE_OPENAI_API_KEY' 74 | value: azureOpenAIKey 75 | } 76 | { 77 | name: 'AZURE_OPENAI_ENDPOINT' 78 | value: azureOpenAIEndpoint 79 | } 80 | { 81 | name: 'AZURE_OPENAI_DEPLOYMENT_NAME' 82 | value: azureOpenAIDeploymentName 83 | } 84 | ] 85 | ftpsState: 'FtpsOnly' 86 | } 87 | } 88 | } 89 | 90 | // Register your web service as a bot with the Bot Framework 91 | module azureBotRegistration './botRegistration/azurebot.bicep' = { 92 | name: 'Azure-Bot-registration' 93 | params: { 94 | resourceBaseName: resourceBaseName 95 | botAadAppClientId: botAadAppClientId 96 | botAppDomain: webApp.properties.defaultHostName 97 | botDisplayName: botDisplayName 98 | } 99 | } 100 | 101 | // The output will be persisted in .env.{envName}. Visit https://aka.ms/teamsfx-actions/arm-deploy for more details. 102 | output BOT_AZURE_APP_SERVICE_RESOURCE_ID string = webApp.id 103 | output BOT_DOMAIN string = webApp.properties.defaultHostName 104 | -------------------------------------------------------------------------------- /src/custom-engine-agent/infra/azure.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "resourceBaseName": { 6 | "value": "bot${{RESOURCE_SUFFIX}}" 7 | }, 8 | "botAadAppClientId": { 9 | "value": "${{BOT_ID}}" 10 | }, 11 | "botAadAppClientSecret": { 12 | "value": "${{SECRET_BOT_PASSWORD}}" 13 | }, 14 | "azureOpenAIKey": { 15 | "value": "${{SECRET_AZURE_OPENAI_API_KEY}}" 16 | }, 17 | "azureOpenAIEndpoint": { 18 | "value": "${{AZURE_OPENAI_ENDPOINT}}" 19 | }, 20 | "azureOpenAIDeploymentName": { 21 | "value": "${{AZURE_OPENAI_DEPLOYMENT_NAME}}" 22 | }, 23 | "webAppSKU": { 24 | "value": "B1" 25 | }, 26 | "botDisplayName": { 27 | "value": "CareerGenie" 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/custom-engine-agent/infra/botRegistration/azurebot.bicep: -------------------------------------------------------------------------------- 1 | @maxLength(20) 2 | @minLength(4) 3 | @description('Used to generate names for all resources in this file') 4 | param resourceBaseName string 5 | 6 | @maxLength(42) 7 | param botDisplayName string 8 | 9 | param botServiceName string = resourceBaseName 10 | param botServiceSku string = 'F0' 11 | param botAadAppClientId string 12 | param botAppDomain string 13 | 14 | // Register your web service as a bot with the Bot Framework 15 | resource botService 'Microsoft.BotService/botServices@2021-03-01' = { 16 | kind: 'azurebot' 17 | location: 'global' 18 | name: botServiceName 19 | properties: { 20 | displayName: botDisplayName 21 | endpoint: 'https://${botAppDomain}/api/messages' 22 | msaAppId: botAadAppClientId 23 | } 24 | sku: { 25 | name: botServiceSku 26 | } 27 | } 28 | 29 | // Connect the bot service to Microsoft Teams 30 | resource botServiceMsTeamsChannel 'Microsoft.BotService/botServices/channels@2021-03-01' = { 31 | parent: botService 32 | location: 'global' 33 | name: 'MsTeamsChannel' 34 | properties: { 35 | channelName: 'MsTeamsChannel' 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/custom-engine-agent/infra/botRegistration/readme.md: -------------------------------------------------------------------------------- 1 | The `azurebot.bicep` module is provided to help you create Azure Bot service when you don't use Azure to host your app. If you use Azure as infrastrcture for your app, `azure.bicep` under infra folder already leverages this module to create Azure Bot service for you. You don't need to deploy `azurebot.bicep` again. -------------------------------------------------------------------------------- /src/custom-engine-agent/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "careergenie", 3 | "version": "1.0.0", 4 | "msteams": { 5 | "teamsAppId": null 6 | }, 7 | "description": "Microsoft Teams Toolkit AI Chat Bot Sample with Teams AI Library", 8 | "engines": { 9 | "node": "16 || 18" 10 | }, 11 | "author": "Microsoft", 12 | "license": "MIT", 13 | "main": "./lib/src/index.js", 14 | "scripts": { 15 | "dev:teamsfx": "env-cmd --silent -f .localConfigs npm run dev", 16 | "dev:teamsfx:testtool": "env-cmd --silent -f .localConfigs.testTool npm run dev", 17 | "dev:teamsfx:launch-testtool": "env-cmd --silent -f env/.env.testtool teamsapptester start", 18 | "dev": "nodemon --exec node --inspect=9239 --signal SIGINT -r ts-node/register ./src/index.ts", 19 | "build": "tsc --build && shx cp -r ./src/prompts ./lib/src", 20 | "start": "node ./lib/src/index.js", 21 | "test": "echo \"Error: no test specified\" && exit 1", 22 | "watch": "nodemon --exec \"npm run start\"" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com" 27 | }, 28 | "dependencies": { 29 | "@microsoft/microsoft-graph-client": "^3.0.7", 30 | "@microsoft/microsoft-graph-types": "^2.40.0", 31 | "@microsoft/teams-ai": "^1.5.3", 32 | "botbuilder": "^4.23.1", 33 | "openai": "^4.68.2", 34 | "restify": "^10.0.0" 35 | }, 36 | "devDependencies": { 37 | "@types/node": "^18.0.0", 38 | "@types/restify": "^8.5.5", 39 | "env-cmd": "^10.1.0", 40 | "nodemon": "^2.0.22", 41 | "shx": "^0.3.3", 42 | "ts-node": "^10.4.0", 43 | "typescript": "^4.4.4" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/custom-engine-agent/prompts/chat/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema": 1.1, 3 | "description": "A bot that can chat with users", 4 | "type": "completion", 5 | "completion": { 6 | "completion_type": "chat", 7 | "include_history": true, 8 | "include_input": true, 9 | "max_input_tokens": 2800, 10 | "max_tokens": 1000, 11 | "temperature": 0.9, 12 | "top_p": 0.0, 13 | "presence_penalty": 0.6, 14 | "frequency_penalty": 0.0, 15 | "data_sources": [ 16 | { 17 | "type": "azure_search", 18 | "parameters": { 19 | "endpoint": "$searchEndpoint", 20 | "index_name": "$indexName", 21 | "authentication": { 22 | "type": "api_key", 23 | "key": "$searchApiKey" 24 | }, 25 | "query_type":"vector", 26 | "in_scope": true, 27 | "strictness": 3, 28 | "top_n_documents": 3, 29 | "embedding_dependency": { 30 | "type": "deployment_name", 31 | "deployment_name": "$azureOpenAIEmbeddingDeploymentName" 32 | } 33 | } 34 | } 35 | ] 36 | } 37 | } -------------------------------------------------------------------------------- /src/custom-engine-agent/prompts/chat/skprompt.txt: -------------------------------------------------------------------------------- 1 | You are a career specialist named "Career Genie" that helps Human Resources team for finding the right candidate for the jobs. 2 | You are friendly and professional. 3 | You always greet users with excitement and introduce yourself first. 4 | You like using emojis where appropriate. 5 | Always mention all citations in your content. -------------------------------------------------------------------------------- /src/custom-engine-agent/prompts/monologue/actions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "createList", 4 | "description": "Creates a new list with an optional set of initial Candidates", 5 | "parameters": { 6 | "type": "object", 7 | "properties": { 8 | "list": { 9 | "type": "string", 10 | "description": "The name of the list to create" 11 | }, 12 | "Candidates": { 13 | "type": "array", 14 | "description": "The Candidates to add to the list", 15 | "Candidates": { 16 | "type": "string" 17 | } 18 | } 19 | }, 20 | "required": [ 21 | "list" 22 | ] 23 | } 24 | }, 25 | { 26 | "name": "deleteList", 27 | "description": "Deletes a list", 28 | "parameters": { 29 | "type": "object", 30 | "properties": { 31 | "list": { 32 | "type": "string", 33 | "description": "The name of the list to delete" 34 | } 35 | }, 36 | "required": [ 37 | "list" 38 | ] 39 | } 40 | }, 41 | { 42 | "name": "addCandidates", 43 | "description": "Adds one or more Candidates to a list", 44 | "parameters": { 45 | "type": "object", 46 | "properties": { 47 | "list": { 48 | "type": "string", 49 | "description": "The name of the list to add the item to" 50 | }, 51 | "Candidates": { 52 | "type": "array", 53 | "description": "The Candidates to add to the list", 54 | "Candidates": { 55 | "type": "string" 56 | } 57 | } 58 | }, 59 | "required": [ 60 | "list", 61 | "Candidates" 62 | ] 63 | } 64 | }, 65 | { 66 | "name": "removeCandidates", 67 | "description": "Removes one or more Candidates from a list", 68 | "parameters": { 69 | "type": "object", 70 | "properties": { 71 | "list": { 72 | "type": "string", 73 | "description": "The name of the list to remove the item from" 74 | }, 75 | "Candidates": { 76 | "type": "array", 77 | "description": "The Candidates to remove from the list", 78 | "Candidates": { 79 | "type": "string" 80 | } 81 | } 82 | }, 83 | "required": [ 84 | "list", 85 | "Candidates" 86 | ] 87 | } 88 | }, 89 | { 90 | "name": "sendLists", 91 | "description": "Send list of Candidates to Human Resources, aka HR for scheduling interviews", 92 | "parameters": { 93 | "type": "object", 94 | "properties": { 95 | "list": { 96 | "type": "string", 97 | "description": "The name of the list to send Human Resources, aka HR for scheduling interviews" 98 | }, 99 | "Candidates": { 100 | "type": "array", 101 | "description": "The Candidates in the list to send Human Resources, aka HR for scheduling interviews", 102 | "Candidates": { 103 | "type": "string" 104 | } 105 | } 106 | }, 107 | "required": [ 108 | "list", 109 | "Candidates" 110 | ] 111 | } 112 | } 113 | ] -------------------------------------------------------------------------------- /src/custom-engine-agent/prompts/monologue/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema": 1.1, 3 | "description": "A bot that can chat with users", 4 | "type": "completion", 5 | "completion": { 6 | "completion_type": "chat", 7 | "include_history": true, 8 | "include_input": true, 9 | "max_input_tokens": 2800, 10 | "max_tokens": 1000, 11 | "temperature": 0.9, 12 | "top_p": 0.0, 13 | "presence_penalty": 0.6, 14 | "frequency_penalty": 0.0 15 | }, 16 | "augmentation": { 17 | "augmentation_type": "monologue" 18 | } 19 | } -------------------------------------------------------------------------------- /src/custom-engine-agent/prompts/monologue/skprompt.txt: -------------------------------------------------------------------------------- 1 | You are a career specialist named "Career Genie" that helps Human Resources team who can manage lists of Candidates. 2 | You are friendly and professional. You like using emojis where appropriate. 3 | Always share the lists in bullet points. 4 | 5 | rules: 6 | - only create lists the user has explicitly asked to create. 7 | - only add Candidates to a list that the user has asked to have added. 8 | - if multiple lists are being manipulated, call a separate action for each list. 9 | - if Candidates are being added and removed from a list, call a separate action for each operation. 10 | - if user asks for a summary, share all the lists and candidates. 11 | - only send an email to HR if user has explicitly asked to send. 12 | 13 | Current lists: 14 | {{$conversation.lists}} -------------------------------------------------------------------------------- /src/custom-engine-agent/public/auth-end.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Login End Page 4 | 5 | 6 | 7 | 8 | 9 | 14 |
15 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/custom-engine-agent/src/adapter.ts: -------------------------------------------------------------------------------- 1 | // Import required bot services. 2 | // See https://aka.ms/bot-services to learn more about the different parts of a bot. 3 | import { 4 | CloudAdapter, 5 | ConfigurationBotFrameworkAuthentication, 6 | ConfigurationServiceClientCredentialFactory, 7 | } from "botbuilder"; 8 | 9 | // This bot's main dialog. 10 | import config from "./config"; 11 | import { TeamsAdapter } from '@microsoft/teams-ai'; 12 | // const botFrameworkAuthentication = new ConfigurationBotFrameworkAuthentication( 13 | // {}, 14 | // new ConfigurationServiceClientCredentialFactory({ 15 | // MicrosoftAppId: config.botId, 16 | // MicrosoftAppPassword: process.env.BOT_PASSWORD, 17 | // MicrosoftAppType: "MultiTenant", 18 | // }) 19 | // ); 20 | 21 | // Create adapter. 22 | // See https://aka.ms/about-bot-adapter to learn more about how bots work. 23 | const adapter = new TeamsAdapter( 24 | {}, 25 | new ConfigurationServiceClientCredentialFactory({ 26 | MicrosoftAppId: config.botId, 27 | MicrosoftAppPassword: process.env.BOT_PASSWORD, 28 | MicrosoftAppType: 'MultiTenant', 29 | }) 30 | ); 31 | 32 | // Catch-all for errors. 33 | const onTurnErrorHandler = async (context, error) => { 34 | // This check writes out errors to console log .vs. app insights. 35 | // NOTE: In production environment, you should consider logging this to Azure 36 | // application insights. 37 | console.error(`\n [onTurnError] unhandled error: ${error}`); 38 | 39 | // Only send error message for user messages, not for other message types so the bot doesn't spam a channel or chat. 40 | if (context.activity.type === "message") { 41 | // Send a trace activity, which will be displayed in Bot Framework Emulator 42 | await context.sendTraceActivity( 43 | "OnTurnError Trace", 44 | `${error}`, 45 | "https://www.botframework.com/schemas/error", 46 | "TurnError" 47 | ); 48 | 49 | // Send a message to the user 50 | await context.sendActivity("The bot encountered an error or bug."); 51 | await context.sendActivity("To continue to run this bot, please fix the bot source code."); 52 | } 53 | }; 54 | 55 | // Set the onTurnError for the singleton CloudAdapter. 56 | adapter.onTurnError = onTurnErrorHandler; 57 | 58 | export default adapter; 59 | -------------------------------------------------------------------------------- /src/custom-engine-agent/src/app/actions.ts: -------------------------------------------------------------------------------- 1 | import { getUserDisplayName, ApplicationTurnState } from './app'; 2 | import { Client } from "@microsoft/microsoft-graph-client"; 3 | import config from '../config'; 4 | 5 | function getCandidates(state: ApplicationTurnState, list: string): string[] { 6 | ensureListExists(state, list); 7 | return state.conversation.lists[list]; 8 | } 9 | 10 | function setCandidates(state: ApplicationTurnState, list: string, Candidates: string[]): void { 11 | ensureListExists(state, list); 12 | state.conversation.lists[list] = Candidates ?? []; 13 | } 14 | 15 | function ensureListExists(state: ApplicationTurnState, listName: string): void { 16 | if (typeof state.conversation.lists != 'object') { 17 | state.conversation.lists = {}; 18 | } 19 | 20 | if (!Object.prototype.hasOwnProperty.call(state.conversation.lists, listName)) { 21 | state.conversation.lists[listName] = []; 22 | } 23 | } 24 | 25 | function deleteList(state: ApplicationTurnState, listName: string): void { 26 | if ( 27 | typeof state.conversation.lists == 'object' && 28 | Object.prototype.hasOwnProperty.call(state.conversation.lists, listName) 29 | ) { 30 | delete state.conversation.lists[listName]; 31 | } 32 | } 33 | 34 | async function sendLists(state: ApplicationTurnState, token): Promise { 35 | const email = await createEmailContent(state.conversation.lists, token); 36 | try { 37 | const client = Client.init({ 38 | authProvider: (done) => { 39 | done(null, token); 40 | } 41 | }); 42 | const sendEmail = await client.api('/me/sendMail').post(JSON.stringify(email)); 43 | if (sendEmail.ok) { 44 | return email.message.body.content; 45 | } 46 | else { 47 | console.log(`Error ${sendEmail.status} calling Graph in sendToHR: ${sendEmail.statusText}`); 48 | return 'Error sending email'; 49 | } 50 | } catch (error) { 51 | console.error('Error in sendLists:', error); 52 | throw error; 53 | } 54 | } 55 | 56 | async function createEmailContent(lists, token) { 57 | let emailContent = ''; 58 | for (const listName in lists) { 59 | if (lists.hasOwnProperty(listName)) { 60 | emailContent += `${listName}:\n`; 61 | lists[listName].forEach(candidate => { 62 | emailContent += ` • ${candidate}\n`; 63 | }); 64 | emailContent += '\n'; // Add an extra line between different lists 65 | } 66 | } 67 | 68 | const profileName = await getUserDisplayName(token); 69 | 70 | const email = { 71 | "message": { 72 | "subject": "Request to Schedule Interviews with Shortlisted Candidates", 73 | "body": { 74 | "contentType": "Text", 75 | "content": `Hello HR Team, \nI hope this email finds you well. \n\nCould you please assist in scheduling 1:1 interviews with the following shortlisted candidates? \n\n${emailContent} Please arrange suitable times and send out the calendar invites accordingly. \n\n Best Regards, \n ${profileName}` 76 | }, 77 | "toRecipients": [ 78 | { 79 | "emailAddress": { 80 | "address": `${config.HR_EMAIL}` 81 | } 82 | } 83 | ] 84 | }, 85 | "saveToSentCandidates": "true" 86 | }; 87 | return await email; 88 | } 89 | 90 | export { getCandidates, setCandidates, ensureListExists, deleteList, sendLists }; -------------------------------------------------------------------------------- /src/custom-engine-agent/src/app/card.ts: -------------------------------------------------------------------------------- 1 | import { AdaptiveCard, Message, Utilities } from '@microsoft/teams-ai'; 2 | /** 3 | * Create an adaptive card from a prompt response. 4 | * @param {Message} response The prompt response to create the card from. 5 | * @returns {AdaptiveCard} The response card. 6 | */ 7 | 8 | //Adaptive card to display the response and citations 9 | export function createResponseCard(response: Message): AdaptiveCard { 10 | const citationCards = response.context?.citations.map((citation, i) => ({ 11 | type: 'Action.ShowCard', 12 | title: `${i+1}`, 13 | card: { 14 | type: 'AdaptiveCard', 15 | body: [ 16 | { 17 | type: 'TextBlock', 18 | text: citation.title, 19 | fontType: 'Default', 20 | weight: 'Bolder' 21 | }, 22 | { 23 | type: 'TextBlock', 24 | text: citation.content, 25 | wrap: true 26 | } 27 | ] 28 | } 29 | })); 30 | 31 | const text = Utilities.formatCitationsResponse(response.content!); 32 | return { 33 | type: 'AdaptiveCard', 34 | body: [ 35 | { 36 | type: 'TextBlock', 37 | text: text, 38 | wrap: true 39 | }, 40 | { 41 | type: 'TextBlock', 42 | text: 'Citations', 43 | wrap: true, 44 | fontType: 'Default', 45 | weight: 'Bolder' 46 | }, 47 | { 48 | type: 'ActionSet', 49 | actions: citationCards 50 | } 51 | ], 52 | $schema: 'http://adaptivecards.io/schemas/adaptive-card.json', 53 | version: '1.5' 54 | }; 55 | } 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/custom-engine-agent/src/config.ts: -------------------------------------------------------------------------------- 1 | const config = { 2 | botId: process.env.BOT_ID, 3 | botPassword: process.env.BOT_PASSWORD, 4 | azureOpenAIKey: process.env.AZURE_OPENAI_API_KEY, 5 | azureOpenAIEndpoint: process.env.AZURE_OPENAI_ENDPOINT, 6 | azureOpenAIDeploymentName: process.env.AZURE_OPENAI_DEPLOYMENT_NAME, 7 | azureOpenAIEmbeddingDeploymentName: process.env.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME, 8 | azureSearchKey: process.env.AZURE_SEARCH_KEY, 9 | azureSearchEndpoint: process.env.AZURE_SEARCH_ENDPOINT, 10 | indexName: process.env.INDEX_NAME, 11 | aadAppClientId: process.env.AAD_APP_CLIENT_ID, 12 | aadAppClientSecret: process.env.AAD_APP_CLIENT_SECRET, 13 | aadAppOauthAuthorityHost: process.env.AAD_APP_OAUTH_AUTHORITY_HOST, 14 | aadAppTenantId: process.env.AAD_APP_TENANT_ID, 15 | botDomain: process.env.BOT_DOMAIN, 16 | aadAppOauthAuthority: process.env.AAD_APP_OAUTH_AUTHORITY, 17 | HR_EMAIL: process.env.HR_EMAIL, 18 | }; 19 | 20 | export default config; 21 | -------------------------------------------------------------------------------- /src/custom-engine-agent/src/index.ts: -------------------------------------------------------------------------------- 1 | // Import required packages 2 | import * as restify from "restify"; 3 | import * as path from 'path'; 4 | // This bot's adapter 5 | import adapter from "./adapter"; 6 | 7 | // This bot's main dialog. 8 | import app from "./app/app"; 9 | 10 | // Create HTTP server. 11 | const server = restify.createServer(); 12 | server.use(restify.plugins.bodyParser()); 13 | 14 | server.listen(process.env.port || process.env.PORT || 3978, () => { 15 | console.log(`\nBot Started, ${server.name} listening to ${server.url}`); 16 | }); 17 | server.get( 18 | '/auth-:name(start|end).html', 19 | restify.plugins.serveStatic({ 20 | directory: path.join(__dirname, 'public') 21 | }) 22 | ); 23 | 24 | // Listen for incoming server requests. 25 | server.post("/api/messages", async (req, res) => { 26 | // Route received a request to adapter for processing 27 | await adapter.process(req, res as any, async (context) => { 28 | // Dispatch to application for routing 29 | await app.run(context); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/custom-engine-agent/src/prompts/chat/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema": 1.1, 3 | "description": "A bot that can chat with users", 4 | "type": "completion", 5 | "completion": { 6 | "completion_type": "chat", 7 | "include_history": true, 8 | "include_input": true, 9 | "max_input_tokens": 2800, 10 | "max_tokens": 1000, 11 | "temperature": 0.9, 12 | "top_p": 0.0, 13 | "presence_penalty": 0.6, 14 | "frequency_penalty": 0.0, 15 | "data_sources": [ 16 | { 17 | "type": "azure_search", 18 | "parameters": { 19 | "endpoint": "$searchEndpoint", 20 | "index_name": "$indexName", 21 | "authentication": { 22 | "type": "api_key", 23 | "key": "$searchApiKey" 24 | }, 25 | "query_type":"vector", 26 | "in_scope": true, 27 | "strictness": 3, 28 | "top_n_documents": 3, 29 | "embedding_dependency": { 30 | "type": "deployment_name", 31 | "deployment_name": "$azureOpenAIEmbeddingDeploymentName" 32 | } 33 | } 34 | } 35 | ] 36 | } 37 | } -------------------------------------------------------------------------------- /src/custom-engine-agent/src/prompts/chat/skprompt.txt: -------------------------------------------------------------------------------- 1 | You are a career specialist named "Career Genie" that helps Human Resources team for finding the right candidate for the jobs. 2 | You are friendly and professional. 3 | You always greet users with excitement and introduce yourself first. 4 | You like using emojis where appropriate. 5 | Always mention all citations in your content. -------------------------------------------------------------------------------- /src/custom-engine-agent/src/prompts/monologue/actions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "createList", 4 | "description": "Creates a new list with an optional set of initial Candidates", 5 | "parameters": { 6 | "type": "object", 7 | "properties": { 8 | "list": { 9 | "type": "string", 10 | "description": "The name of the list to create" 11 | }, 12 | "Candidates": { 13 | "type": "array", 14 | "description": "The Candidates to add to the list", 15 | "Candidates": { 16 | "type": "string" 17 | } 18 | } 19 | }, 20 | "required": [ 21 | "list" 22 | ] 23 | } 24 | }, 25 | { 26 | "name": "deleteList", 27 | "description": "Deletes a list", 28 | "parameters": { 29 | "type": "object", 30 | "properties": { 31 | "list": { 32 | "type": "string", 33 | "description": "The name of the list to delete" 34 | } 35 | }, 36 | "required": [ 37 | "list" 38 | ] 39 | } 40 | }, 41 | { 42 | "name": "addCandidates", 43 | "description": "Adds one or more Candidates to a list", 44 | "parameters": { 45 | "type": "object", 46 | "properties": { 47 | "list": { 48 | "type": "string", 49 | "description": "The name of the list to add the item to" 50 | }, 51 | "Candidates": { 52 | "type": "array", 53 | "description": "The Candidates to add to the list", 54 | "Candidates": { 55 | "type": "string" 56 | } 57 | } 58 | }, 59 | "required": [ 60 | "list", 61 | "Candidates" 62 | ] 63 | } 64 | }, 65 | { 66 | "name": "removeCandidates", 67 | "description": "Removes one or more Candidates from a list", 68 | "parameters": { 69 | "type": "object", 70 | "properties": { 71 | "list": { 72 | "type": "string", 73 | "description": "The name of the list to remove the item from" 74 | }, 75 | "Candidates": { 76 | "type": "array", 77 | "description": "The Candidates to remove from the list", 78 | "Candidates": { 79 | "type": "string" 80 | } 81 | } 82 | }, 83 | "required": [ 84 | "list", 85 | "Candidates" 86 | ] 87 | } 88 | }, 89 | { 90 | "name": "sendLists", 91 | "description": "Send list of Candidates to Human Resources, aka HR for scheduling interviews", 92 | "parameters": { 93 | "type": "object", 94 | "properties": { 95 | "list": { 96 | "type": "string", 97 | "description": "The name of the list to send Human Resources, aka HR for scheduling interviews" 98 | }, 99 | "Candidates": { 100 | "type": "array", 101 | "description": "The Candidates in the list to send Human Resources, aka HR for scheduling interviews", 102 | "Candidates": { 103 | "type": "string" 104 | } 105 | } 106 | }, 107 | "required": [ 108 | "list", 109 | "Candidates" 110 | ] 111 | } 112 | } 113 | ] -------------------------------------------------------------------------------- /src/custom-engine-agent/src/prompts/monologue/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema": 1.1, 3 | "description": "A bot that can chat with users", 4 | "type": "completion", 5 | "completion": { 6 | "completion_type": "chat", 7 | "include_history": true, 8 | "include_input": true, 9 | "max_input_tokens": 2800, 10 | "max_tokens": 1000, 11 | "temperature": 0.9, 12 | "top_p": 0.0, 13 | "presence_penalty": 0.6, 14 | "frequency_penalty": 0.0 15 | }, 16 | "augmentation": { 17 | "augmentation_type": "monologue" 18 | } 19 | } -------------------------------------------------------------------------------- /src/custom-engine-agent/src/prompts/monologue/skprompt.txt: -------------------------------------------------------------------------------- 1 | You are a career specialist named "Career Genie" that helps Human Resources team who can manage lists of Candidates. 2 | You are friendly and professional. You like using emojis where appropriate. 3 | Always share the lists in bullet points. 4 | 5 | rules: 6 | - only create lists the user has explicitly asked to create. 7 | - only add Candidates to a list that the user has asked to have added. 8 | - if multiple lists are being manipulated, call a separate action for each list. 9 | - if Candidates are being added and removed from a list, call a separate action for each operation. 10 | - if user asks for a summary, share all the lists and candidates. 11 | - only send an email to HR if user has explicitly asked to send. 12 | 13 | Current lists: 14 | {{$conversation.lists}} -------------------------------------------------------------------------------- /src/custom-engine-agent/src/public/auth-end.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Login End Page 4 | 5 | 6 | 7 | 8 | 9 | 14 |
15 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/custom-engine-agent/teamsapp.local.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/yaml.schema.json 2 | # Visit https://aka.ms/teamsfx-v5.0-guide for details on this file 3 | # Visit https://aka.ms/teamsfx-actions for details on actions 4 | version: v1.5 5 | 6 | provision: 7 | - uses: aadApp/create # Creates a new Entra ID (AAD) app to authenticate users if the environment variable that stores clientId is empty 8 | with: 9 | name: CareerGenieBot-aad # Note: when you run aadApp/update, the AAD app name will be updated based on the definition in manifest. If you don't want to change the name, make sure the name in AAD manifest is the same with the name defined here. 10 | generateClientSecret: true # If the value is false, the action will not generate client secret for you 11 | signInAudience: "AzureADMultipleOrgs" # Authenticate users with a Microsoft work or school account in your organization's Azure AD tenant (for example, single tenant). 12 | writeToEnvironmentFile: # Write the information of created resources into environment file for the specified environment variable(s). 13 | clientId: AAD_APP_CLIENT_ID 14 | clientSecret: SECRET_AAD_APP_CLIENT_SECRET # Environment variable that starts with `SECRET_` will be stored to the .env.{envName}.user environment file 15 | objectId: AAD_APP_OBJECT_ID 16 | tenantId: AAD_APP_TENANT_ID 17 | authority: AAD_APP_OAUTH_AUTHORITY 18 | authorityHost: AAD_APP_OAUTH_AUTHORITY_HOST 19 | 20 | # Creates a Teams app 21 | - uses: teamsApp/create 22 | with: 23 | # Teams app name 24 | name: CareerGenie${{APP_NAME_SUFFIX}} 25 | # Write the information of created resources into environment file for 26 | # the specified environment variable(s). 27 | writeToEnvironmentFile: 28 | teamsAppId: TEAMS_APP_ID 29 | 30 | # Automates the creation an Azure AD app registration which is required for a bot. 31 | # The Bot ID (AAD app client ID) and Bot Password (AAD app client secret) are saved to an environment file. 32 | - uses: botAadApp/create 33 | with: 34 | name: CareerGenieAAD 35 | writeToEnvironmentFile: 36 | botId: BOT_ID 37 | botPassword: SECRET_BOT_PASSWORD 38 | 39 | # Automates the creation and configuration of a Bot Framework registration which is required for a bot. 40 | # This configures the bot to use the Azure AD app registration created in the previous step. 41 | # Teams Toolkit automatically creates a local Dev Tunnel URL and updates BOT_ENDPOINT when debugging (F5). 42 | - uses: botFramework/create 43 | with: 44 | botId: ${{BOT_ID}} 45 | name: CareerGenieBot 46 | messagingEndpoint: ${{BOT_ENDPOINT}}/api/messages 47 | description: "" 48 | channels: 49 | - name: msteams 50 | 51 | 52 | - uses: aadApp/update # Apply the AAD manifest to an existing AAD app. Will use the object id in manifest file to determine which AAD app to update. 53 | with: 54 | manifestPath: ./aad.manifest.json # Relative path to teamsfx folder. Environment variables in manifest will be replaced before apply to AAD app 55 | outputFilePath: ./build/aad.manifest.${{TEAMSFX_ENV}}.json 56 | 57 | # Validate using manifest schema 58 | - uses: teamsApp/validateManifest 59 | with: 60 | # Path to manifest template 61 | manifestPath: ./appPackage/manifest.json 62 | 63 | # Build Teams app package with latest env value 64 | - uses: teamsApp/zipAppPackage 65 | with: 66 | # Path to manifest template 67 | manifestPath: ./appPackage/manifest.json 68 | outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip 69 | outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json 70 | # Validate app package using validation rules 71 | - uses: teamsApp/validateAppPackage 72 | with: 73 | # Relative path to this file. This is the path for built zip file. 74 | appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip 75 | 76 | # Apply the Teams app manifest to an existing Teams app in 77 | # Teams Developer Portal. 78 | # Will use the app id in manifest file to determine which Teams app to update. 79 | - uses: teamsApp/update 80 | with: 81 | # Relative path to this file. This is the path for built zip file. 82 | appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip 83 | 84 | deploy: 85 | # Run npm command 86 | - uses: cli/runNpmCommand 87 | name: install dependencies 88 | with: 89 | args: install --no-audit 90 | 91 | # Generate runtime environment variables 92 | - uses: file/createOrUpdateEnvironmentFile 93 | with: 94 | target: ./.localConfigs 95 | envs: 96 | BOT_ID: ${{BOT_ID}} 97 | BOT_PASSWORD: ${{SECRET_BOT_PASSWORD}} 98 | BOT_DOMAIN: ${{BOT_DOMAIN}} 99 | AZURE_OPENAI_API_KEY: ${{SECRET_AZURE_OPENAI_API_KEY}} 100 | AZURE_OPENAI_ENDPOINT: ${{AZURE_OPENAI_ENDPOINT}} 101 | AZURE_OPENAI_DEPLOYMENT_NAME: ${{AZURE_OPENAI_DEPLOYMENT_NAME}} 102 | AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME: ${{AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME}} 103 | AZURE_SEARCH_KEY: ${{SECRET_AZURE_SEARCH_KEY}} 104 | AZURE_SEARCH_ENDPOINT: ${{AZURE_SEARCH_ENDPOINT}} 105 | INDEX_NAME: ${{INDEX_NAME}} 106 | AAD_APP_CLIENT_ID: ${{AAD_APP_CLIENT_ID}} 107 | AAD_APP_CLIENT_SECRET: ${{SECRET_AAD_APP_CLIENT_SECRET}} 108 | AAD_APP_TENANT_ID: ${{AAD_APP_TENANT_ID}} 109 | AAD_APP_OAUTH_AUTHORITY_HOST: ${{AAD_APP_OAUTH_AUTHORITY_HOST}} 110 | AAD_APP_OAUTH_AUTHORITY: ${{AAD_APP_OAUTH_AUTHORITY}} 111 | HR_EMAIL: ${{HR_EMAIL}} 112 | -------------------------------------------------------------------------------- /src/custom-engine-agent/teamsapp.testtool.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/yaml.schema.json 2 | # Visit https://aka.ms/teamsfx-v5.0-guide for details on this file 3 | # Visit https://aka.ms/teamsfx-actions for details on actions 4 | version: v1.5 5 | 6 | deploy: 7 | # Install development tool(s) 8 | - uses: devTool/install 9 | with: 10 | testTool: 11 | version: ~0.2.1 12 | symlinkDir: ./devTools/teamsapptester 13 | 14 | # Run npm command 15 | - uses: cli/runNpmCommand 16 | with: 17 | args: install --no-audit 18 | 19 | # Generate runtime environment variables 20 | - uses: file/createOrUpdateEnvironmentFile 21 | with: 22 | target: ./.localConfigs.testTool 23 | envs: 24 | AZURE_OPENAI_API_KEY: ${{SECRET_AZURE_OPENAI_API_KEY}} 25 | AZURE_OPENAI_ENDPOINT: ${{AZURE_OPENAI_ENDPOINT}} 26 | AZURE_OPENAI_DEPLOYMENT_NAME: ${{AZURE_OPENAI_DEPLOYMENT_NAME}} 27 | TEAMSFX_NOTIFICATION_STORE_FILENAME: ${{TEAMSFX_NOTIFICATION_STORE_FILENAME}} -------------------------------------------------------------------------------- /src/custom-engine-agent/teamsapp.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/yaml.schema.json 2 | # Visit https://aka.ms/teamsfx-v5.0-guide for details on this file 3 | # Visit https://aka.ms/teamsfx-actions for details on actions 4 | version: v1.5 5 | 6 | environmentFolderPath: ./env 7 | 8 | # Triggered when 'teamsapp provision' is executed 9 | provision: 10 | # Creates a Teams app 11 | - uses: teamsApp/create 12 | with: 13 | # Teams app name 14 | name: CareerGenie${{APP_NAME_SUFFIX}} 15 | # Write the information of created resources into environment file for 16 | # the specified environment variable(s). 17 | writeToEnvironmentFile: 18 | teamsAppId: TEAMS_APP_ID 19 | 20 | # Create or reuse an existing Microsoft Entra application for bot. 21 | - uses: aadApp/create 22 | with: 23 | # The Microsoft Entra application's display name 24 | name: CareerGenie${{APP_NAME_SUFFIX}} 25 | generateClientSecret: true 26 | signInAudience: AzureADMultipleOrgs 27 | writeToEnvironmentFile: 28 | # The Microsoft Entra application's client id created for bot. 29 | clientId: BOT_ID 30 | # The Microsoft Entra application's client secret created for bot. 31 | clientSecret: SECRET_BOT_PASSWORD 32 | # The Microsoft Entra application's object id created for bot. 33 | objectId: BOT_OBJECT_ID 34 | 35 | - uses: arm/deploy # Deploy given ARM templates parallelly. 36 | with: 37 | # AZURE_SUBSCRIPTION_ID is a built-in environment variable, 38 | # if its value is empty, TeamsFx will prompt you to select a subscription. 39 | # Referencing other environment variables with empty values 40 | # will skip the subscription selection prompt. 41 | subscriptionId: ${{AZURE_SUBSCRIPTION_ID}} 42 | # AZURE_RESOURCE_GROUP_NAME is a built-in environment variable, 43 | # if its value is empty, TeamsFx will prompt you to select or create one 44 | # resource group. 45 | # Referencing other environment variables with empty values 46 | # will skip the resource group selection prompt. 47 | resourceGroupName: ${{AZURE_RESOURCE_GROUP_NAME}} 48 | templates: 49 | - path: ./infra/azure.bicep # Relative path to this file 50 | # Relative path to this yaml file. 51 | # Placeholders will be replaced with corresponding environment 52 | # variable before ARM deployment. 53 | parameters: ./infra/azure.parameters.json 54 | # Required when deploying ARM template 55 | deploymentName: Create-resources-for-bot 56 | # Teams Toolkit will download this bicep CLI version from github for you, 57 | # will use bicep CLI in PATH if you remove this config. 58 | bicepCliVersion: v0.9.1 59 | 60 | # Validate using manifest schema 61 | - uses: teamsApp/validateManifest 62 | with: 63 | # Path to manifest template 64 | manifestPath: ./appPackage/manifest.json 65 | # Build Teams app package with latest env value 66 | - uses: teamsApp/zipAppPackage 67 | with: 68 | # Path to manifest template 69 | manifestPath: ./appPackage/manifest.json 70 | outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip 71 | outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json 72 | # Validate app package using validation rules 73 | - uses: teamsApp/validateAppPackage 74 | with: 75 | # Relative path to this file. This is the path for built zip file. 76 | appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip 77 | # Apply the Teams app manifest to an existing Teams app in 78 | # Teams Developer Portal. 79 | # Will use the app id in manifest file to determine which Teams app to update. 80 | - uses: teamsApp/update 81 | with: 82 | # Relative path to this file. This is the path for built zip file. 83 | appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip 84 | 85 | # Triggered when 'teamsapp deploy' is executed 86 | deploy: 87 | # Run npm command 88 | - uses: cli/runNpmCommand 89 | name: install dependencies 90 | with: 91 | args: install 92 | - uses: cli/runNpmCommand 93 | name: build app 94 | with: 95 | args: run build --if-present 96 | # Deploy your application to Azure App Service using the zip deploy feature. 97 | # For additional details, refer to https://aka.ms/zip-deploy-to-app-services. 98 | - uses: azureAppService/zipDeploy 99 | with: 100 | # Deploy base folder 101 | artifactFolder: . 102 | # Ignore file location, leave blank will ignore nothing 103 | ignoreFile: .webappignore 104 | # The resource id of the cloud resource to be deployed to. 105 | # This key will be generated by arm/deploy action automatically. 106 | # You can replace it with your existing Azure Resource id 107 | # or add it to your environment variable file. 108 | resourceId: ${{BOT_AZURE_APP_SERVICE_RESOURCE_ID}} 109 | 110 | # Triggered when 'teamsapp publish' is executed 111 | publish: 112 | # Validate using manifest schema 113 | - uses: teamsApp/validateManifest 114 | with: 115 | # Path to manifest template 116 | manifestPath: ./appPackage/manifest.json 117 | # Build Teams app package with latest env value 118 | - uses: teamsApp/zipAppPackage 119 | with: 120 | # Path to manifest template 121 | manifestPath: ./appPackage/manifest.json 122 | outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip 123 | outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json 124 | # Validate app package using validation rules 125 | - uses: teamsApp/validateAppPackage 126 | with: 127 | # Relative path to this file. This is the path for built zip file. 128 | appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip 129 | # Apply the Teams app manifest to an existing Teams app in 130 | # Teams Developer Portal. 131 | # Will use the app id in manifest file to determine which Teams app to update. 132 | - uses: teamsApp/update 133 | with: 134 | # Relative path to this file. This is the path for built zip file. 135 | appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip 136 | # Publish the app to 137 | # Teams Admin Center (https://admin.teams.microsoft.com/policies/manage-apps) 138 | # for review and approval 139 | - uses: teamsApp/publishAppPackage 140 | with: 141 | appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip 142 | # Write the information of created resources into environment file for 143 | # the specified environment variable(s). 144 | writeToEnvironmentFile: 145 | publishedAppId: TEAMS_APP_PUBLISHED_APP_ID 146 | projectId: 99b60261-b586-4dde-98ba-7ea5cefafa46 147 | -------------------------------------------------------------------------------- /src/custom-engine-agent/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "target": "es2017", 5 | "module": "commonjs", 6 | "outDir": "./lib", 7 | "rootDir": "./", 8 | "sourceMap": true, 9 | "incremental": true, 10 | "tsBuildInfoFile": "./lib/.tsbuildinfo", 11 | "esModuleInterop": true 12 | } 13 | } -------------------------------------------------------------------------------- /src/custom-engine-agent/web.config: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/declarative-agent/.funcignore: -------------------------------------------------------------------------------- 1 | .funcignore 2 | *.js.map 3 | *.ts 4 | .git* 5 | .localConfigs 6 | .vscode 7 | local.settings.json 8 | test 9 | tsconfig.json 10 | .DS_Store 11 | .deployment 12 | node_modules/.bin 13 | node_modules/azure-functions-core-tools 14 | README.md 15 | tsconfig.json 16 | teamsapp.yml 17 | teamsapp.*.yml 18 | /env/ 19 | /appPackage/ 20 | /infra/ 21 | /devTools/ -------------------------------------------------------------------------------- /src/declarative-agent/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | # TeamsFx files 3 | env/.env.*.user 4 | env/.env.local 5 | .DS_Store 6 | build 7 | appPackage/build 8 | pluginPackage/build 9 | .deployment 10 | 11 | # dependencies 12 | /node_modules 13 | 14 | # testing 15 | /coverage 16 | 17 | # Dev tool directories 18 | /devTools/ 19 | 20 | # TypeScript output 21 | dist 22 | out 23 | 24 | # Azure Functions artifacts 25 | bin 26 | obj 27 | appsettings.json 28 | 29 | # Local data 30 | .localConfigs 31 | 32 | # Azurite storage 33 | _storage_emulator 34 | 35 | # User-specific files 36 | appPackage/hand-built 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/declarative-agent/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "TeamsDevApp.ms-teams-vscode-extension" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/declarative-agent/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch App in Teams (Edge)", 6 | "type": "msedge", 7 | "request": "launch", 8 | "url": "https://teams.microsoft.com?${account-hint}", 9 | "cascadeTerminateToConfigurations": [ 10 | "Attach to Backend" 11 | ], 12 | "presentation": { 13 | "group": "all", 14 | "hidden": true 15 | }, 16 | "internalConsoleOptions": "neverOpen" 17 | }, 18 | { 19 | "name": "Launch App in Teams (Chrome)", 20 | "type": "chrome", 21 | "request": "launch", 22 | "url": "https://teams.microsoft.com?${account-hint}", 23 | "cascadeTerminateToConfigurations": [ 24 | "Attach to Backend" 25 | ], 26 | "presentation": { 27 | "group": "all", 28 | "hidden": true 29 | }, 30 | "internalConsoleOptions": "neverOpen" 31 | }, 32 | { 33 | "name": "Preview in Copilot (Edge)", 34 | "type": "msedge", 35 | "request": "launch", 36 | "url": "https://teams.microsoft.com?${account-hint}", 37 | "presentation": { 38 | "group": "remote", 39 | "order": 1 40 | }, 41 | "internalConsoleOptions": "neverOpen" 42 | }, 43 | { 44 | "name": "Preview in Copilot (Chrome)", 45 | "type": "chrome", 46 | "request": "launch", 47 | "url": "https://teams.microsoft.com?${account-hint}", 48 | "presentation": { 49 | "group": "remote", 50 | "order": 2 51 | }, 52 | "internalConsoleOptions": "neverOpen" 53 | }, 54 | { 55 | "name": "Attach to Backend", 56 | "type": "node", 57 | "request": "attach", 58 | "port": 9229, 59 | "restart": true, 60 | "presentation": { 61 | "group": "all", 62 | "hidden": true 63 | }, 64 | "internalConsoleOptions": "neverOpen" 65 | } 66 | ], 67 | "compounds": [ 68 | { 69 | "name": "Debug in Copilot (Edge)", 70 | "configurations": [ 71 | "Launch App in Teams (Edge)", 72 | "Attach to Backend" 73 | ], 74 | "preLaunchTask": "Start Teams App Locally", 75 | "presentation": { 76 | "group": "all", 77 | "order": 1 78 | }, 79 | "stopAll": true 80 | }, 81 | { 82 | "name": "Debug in Copilot (Chrome)", 83 | "configurations": [ 84 | "Launch App in Teams (Chrome)", 85 | "Attach to Backend" 86 | ], 87 | "preLaunchTask": "Start Teams App Locally", 88 | "presentation": { 89 | "group": "all", 90 | "order": 2 91 | }, 92 | "stopAll": true 93 | } 94 | ] 95 | } 96 | -------------------------------------------------------------------------------- /src/declarative-agent/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "debug.onTaskErrors": "abort", 3 | "json.schemas": [ 4 | { 5 | "fileMatch": [ 6 | "/aad.*.json" 7 | ], 8 | "schema": {} 9 | } 10 | ], 11 | "azureFunctions.stopFuncTaskPostDebug": false, 12 | "azureFunctions.showProjectWarning": false, 13 | } 14 | -------------------------------------------------------------------------------- /src/declarative-agent/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // This file is automatically generated by Teams Toolkit. 2 | // The teamsfx tasks defined in this file require Teams Toolkit version >= 5.0.0. 3 | // See https://aka.ms/teamsfx-tasks for details on how to customize each task. 4 | { 5 | "version": "2.0.0", 6 | "tasks": [ 7 | { 8 | "label": "Start Azurite emulator", 9 | "type": "shell", 10 | "command": "npm run storage", 11 | "isBackground": true, 12 | "problemMatcher": { 13 | "pattern": [ 14 | { 15 | "regexp": "^.*$", 16 | "file": 0, 17 | "location": 1, 18 | "message": 2 19 | } 20 | ], 21 | "background": { 22 | "activeOnStart": true, 23 | "beginsPattern": "Azurite", 24 | "endsPattern": "successfully listening" 25 | } 26 | }, 27 | "options": { 28 | "cwd": "${workspaceFolder}" 29 | }, 30 | "presentation": { 31 | "reveal": "silent" 32 | } 33 | }, 34 | { 35 | "label": "Start Teams App Locally", 36 | "dependsOn": [ 37 | "Validate prerequisites", 38 | "Start local tunnel", 39 | "Create resources", 40 | "Build project", 41 | "Start application" 42 | ], 43 | "dependsOrder": "sequence" 44 | }, 45 | { 46 | "label": "Validate prerequisites", 47 | "type": "teamsfx", 48 | "command": "debug-check-prerequisites", 49 | "args": { 50 | "prerequisites": [ 51 | "nodejs", 52 | "m365Account", 53 | "portOccupancy" 54 | ], 55 | "portOccupancy": [ 56 | 7071, 57 | 9229 58 | ] 59 | } 60 | }, 61 | { 62 | // Start the local tunnel service to forward public URL to local port and inspect traffic. 63 | // See https://aka.ms/teamsfx-tasks/local-tunnel for the detailed args definitions. 64 | "label": "Start local tunnel", 65 | "dependsOn": [ 66 | "Start Azurite emulator" 67 | ], 68 | "type": "teamsfx", 69 | "command": "debug-start-local-tunnel", 70 | "args": { 71 | "type": "dev-tunnel", 72 | "ports": [ 73 | { 74 | "portNumber": 7071, 75 | "protocol": "http", 76 | "access": "public", 77 | "writeToEnvironmentFile": { 78 | "endpoint": "OPENAPI_SERVER_URL", // output tunnel endpoint as OPENAPI_SERVER_URL 79 | } 80 | } 81 | ], 82 | "env": "local" 83 | }, 84 | "isBackground": true, 85 | "problemMatcher": "$teamsfx-local-tunnel-watch" 86 | }, 87 | { 88 | "label": "Create resources", 89 | "type": "teamsfx", 90 | "command": "provision", 91 | "args": { 92 | "env": "local" 93 | } 94 | }, 95 | { 96 | "label": "Build project", 97 | "type": "teamsfx", 98 | "command": "deploy", 99 | "args": { 100 | "env": "local" 101 | } 102 | }, 103 | { 104 | "label": "Start application", 105 | "dependsOn": [ 106 | "Start backend" 107 | ] 108 | }, 109 | { 110 | "label": "Start backend", 111 | "type": "shell", 112 | "command": "npm run dev:teamsfx", 113 | "isBackground": true, 114 | "options": { 115 | "cwd": "${workspaceFolder}", 116 | "env": { 117 | "PATH": "${workspaceFolder}/devTools/func:${env:PATH}" 118 | } 119 | }, 120 | "windows": { 121 | "options": { 122 | "env": { 123 | "PATH": "${workspaceFolder}/devTools/func;${env:PATH}" 124 | } 125 | } 126 | }, 127 | "problemMatcher": { 128 | "pattern": { 129 | "regexp": "^.*$", 130 | "file": 0, 131 | "location": 1, 132 | "message": 2 133 | }, 134 | "background": { 135 | "activeOnStart": true, 136 | "beginsPattern": "^.*(Job host stopped|signaling restart).*$", 137 | "endsPattern": "^.*(Worker process started and initialized|Host lock lease acquired by instance ID).*$" 138 | } 139 | }, 140 | "presentation": { 141 | "reveal": "silent" 142 | }, 143 | "dependsOn": "Watch backend" 144 | }, 145 | { 146 | "label": "Watch backend", 147 | "type": "shell", 148 | "command": "npm run watch:teamsfx", 149 | "isBackground": true, 150 | "options": { 151 | "cwd": "${workspaceFolder}" 152 | }, 153 | "problemMatcher": "$tsc-watch", 154 | "presentation": { 155 | "reveal": "silent" 156 | } 157 | } 158 | ] 159 | } -------------------------------------------------------------------------------- /src/declarative-agent/README.md: -------------------------------------------------------------------------------- 1 | # Get started with the sample 2 | 3 | Visit [session-delivery-resources](../../session-delivery-resources/declarative-agent/) to learn more about running this sample locally. 4 | 5 | ## Final application 6 | 7 | This folder contains a solution from the [Copilot Developer Camp labs](https://aka.ms/copilotdevcamp). When completed, the solution is a declarative agent called "Trey Genie" which provides assistant to the employees of Trey Research. Trey Research is a fictitious consulting company that supplies talent in the software and pharmaceuticals industries. The vision for this demo is to show the full potential of Copilot extensions in a relatable business environment. 8 | 9 | ### Prompts that work in the completed solution 10 | 11 | * what trey projects am i assigned to? 12 | (NOTE: When authentication is "none" or "API key", the logged in user is assumed to be consultant "Avery Howard". When OAuth is enabled, the logged in user is mapped to user ID 1 in the database, so you will have Avery's projects, etc.) 13 | * what trey projects is domi working on? 14 | * do we have any trey consultants with azure certifications? 15 | * what trey projects are we doing for relecloud? 16 | * which trey consultants are working with woodgrove bank? 17 | * in trey research, how many hours has avery delivered this month? 18 | * please find a trey consultant with python skills who is available immediately 19 | * are any trey research consultants available who are AWS certified? (multi-parameter!) 20 | * does trey research have any architects with javascript skills? (multi-parameter!) 21 | * what trey research designers are working at woodgrove bank? (multi-parameter!) 22 | * please charge 10 hours to woodgrove bank in trey research (POST request) 23 | * please add sanjay to the contoso project for trey research (POST request with easy to forget entities, hoping to prompt the user; for now they are defaulted) 24 | 25 | If the sample files are installed and accessible to the logged-in user, 26 | 27 | * find my hours spreadsheet and get the hours for woodgrove, then bill the client 28 | * make a list of my projects, then write a summary of each based on the statement of work. 29 | 30 | ## API Plugin Features 31 | 32 | The sample aims to showcase the following features of an API plugin used within a Copilot declarative agent: 33 | 34 | 1. API plugin works with any platform that supports REST requests 35 | 1. Construct queries for specific data using GET requests 36 | 1. Multi-parameter queries 37 | 1. Allow updating and adding data using POST requests 38 | 1. Prompt users before POSTing data; capture missing parameters 39 | 1. Invoke from Declarative Copilot, allowing general instructions and knowledge, and removing the need to name the plugin on every prompt 40 | 1. Display rich adaptive cards 41 | 1. Entra ID login with /me path support 42 | -------------------------------------------------------------------------------- /src/declarative-agent/appPackage/TreyResearch-blue-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/src/declarative-agent/appPackage/TreyResearch-blue-192.png -------------------------------------------------------------------------------- /src/declarative-agent/appPackage/TreyResearch-blue-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/src/declarative-agent/appPackage/TreyResearch-blue-32.png -------------------------------------------------------------------------------- /src/declarative-agent/appPackage/extra-icons/TreyResearch-gold-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/src/declarative-agent/appPackage/extra-icons/TreyResearch-gold-192.png -------------------------------------------------------------------------------- /src/declarative-agent/appPackage/extra-icons/TreyResearch-gold-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/src/declarative-agent/appPackage/extra-icons/TreyResearch-gold-32.png -------------------------------------------------------------------------------- /src/declarative-agent/appPackage/extra-icons/TreyResearch-green-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/src/declarative-agent/appPackage/extra-icons/TreyResearch-green-192.png -------------------------------------------------------------------------------- /src/declarative-agent/appPackage/extra-icons/TreyResearch-green-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/src/declarative-agent/appPackage/extra-icons/TreyResearch-green-32.png -------------------------------------------------------------------------------- /src/declarative-agent/appPackage/extra-icons/TreyResearch-red-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/src/declarative-agent/appPackage/extra-icons/TreyResearch-red-192.png -------------------------------------------------------------------------------- /src/declarative-agent/appPackage/extra-icons/TreyResearch-red-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/src/declarative-agent/appPackage/extra-icons/TreyResearch-red-32.png -------------------------------------------------------------------------------- /src/declarative-agent/appPackage/extra-icons/color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/src/declarative-agent/appPackage/extra-icons/color.png -------------------------------------------------------------------------------- /src/declarative-agent/appPackage/extra-icons/outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/src/declarative-agent/appPackage/extra-icons/outline.png -------------------------------------------------------------------------------- /src/declarative-agent/appPackage/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.19/MicrosoftTeams.schema.json", 3 | "manifestVersion": "1.19", 4 | "id": "${{TEAMS_APP_ID}}", 5 | "version": "1.0.0", 6 | "developer": { 7 | "name": "Teams App, Inc.", 8 | "websiteUrl": "https://www.example.com", 9 | "privacyUrl": "https://www.example.com/privacy", 10 | "termsOfUseUrl": "https://www.example.com/termsofuse" 11 | }, 12 | "icons": { 13 | "color": "TreyResearch-blue-192.png", 14 | "outline": "TreyResearch-blue-32.png" 15 | }, 16 | "name": { 17 | "short": "Trey Research E5 ${{APP_NAME_SUFFIX}}", 18 | "full": "Trey Research consulting management" 19 | }, 20 | "description": { 21 | "short": "Everyday management of Trey Research projects and consultants", 22 | "full": "This application allows you to find Trey Research projects and consultants, to bill time to a project, and to assign a consultant to a project." 23 | }, 24 | "accentColor": "#FFFFFF", 25 | "copilotAgents": { 26 | "declarativeAgents": [ 27 | { 28 | "id": "treygenie", 29 | "file": "trey-declarative-agent.json" 30 | } 31 | ] 32 | }, 33 | "permissions": [ 34 | "identity", 35 | "messageTeamMembers" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /src/declarative-agent/appPackage/trey-declarative-agent.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://aka.ms/json-schemas/copilot-extensions/vNext/declarative-copilot.schema.json", 3 | "version": "v1.0", 4 | "name": "Trey Genie Local E5", 5 | "description": "You are a handy assistant for consultants at Trey Research, a boutique consultancy specializing in software development and clinical trials. ", 6 | "instructions": "Greet users in a professional manner, introduce yourself as the Trey Genie, and offer to help them. Always remind users of the Trey motto, 'Always be Billing!'. Your main job is to help consultants with their projects and hours. Using the TreyResearch action, you are able to find consultants based on their names, project assignments, skills, roles, and certifications. You can also find project details based on the project or client name, charge hours on a project, and add a consultant to a project. If a user asks how many hours they have billed, charged, or worked on a project, reword the request to ask how many hours they have delivered. In addition, you may offer general consulting advice. If there is any confusion, encourage users to speak with their Managing Consultant. Avoid giving legal advice.", 7 | "conversation_starters": [ 8 | { 9 | "title": "Find consultants", 10 | "text": "Find consultants with TypeScript skills" 11 | }, 12 | { 13 | "title": "My Projects", 14 | "text": "What projects am I assigned to?" 15 | }, 16 | { 17 | "title": "My Hours", 18 | "text": "How many hours have I delivered on projects this month?" 19 | }, 20 | { 21 | "title": "Find projects for a client", 22 | "text": "What projects are we doing for Alpine Ski House?" 23 | } 24 | ], 25 | "capabilities": [ 26 | { 27 | "name": "OneDriveAndSharePoint", 28 | "items_by_url": [ 29 | { 30 | "url": "${{SHAREPOINT_DOCS_URL}}" 31 | } 32 | ] 33 | } 34 | ], 35 | "actions": [ 36 | { 37 | "id": "treyresearch", 38 | "file": "trey-plugin.json" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /src/declarative-agent/env/.env.dev: -------------------------------------------------------------------------------- 1 | # This file includes environment variables that will be committed to git by default. 2 | 3 | # Built-in environment variables 4 | TEAMSFX_ENV=dev 5 | APP_NAME_SUFFIX=dev 6 | 7 | # 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. 8 | AZURE_SUBSCRIPTION_ID= 9 | AZURE_RESOURCE_GROUP_NAME= 10 | RESOURCE_SUFFIX= 11 | 12 | # Generated during provision, you can also add your own variables. 13 | TEAMS_APP_ID= 14 | TEAMS_APP_PUBLISHED_APP_ID= 15 | TEAMS_APP_TENANT_ID= 16 | API_FUNCTION_ENDPOINT= 17 | API_FUNCTION_RESOURCE_ID= 18 | DOTNET_PATH= -------------------------------------------------------------------------------- /src/declarative-agent/env/.env.local.sample: -------------------------------------------------------------------------------- 1 | SHAREPOINT_DOCS_URL=https://(your-tenant).sharepoint.com/sites/(your-side) -------------------------------------------------------------------------------- /src/declarative-agent/env/.env.local.user.sample: -------------------------------------------------------------------------------- 1 | SECRET_BOT_PASSWORD= 2 | SECRET_STORAGE_ACCOUNT_CONNECTION_STRING=UseDevelopmentStorage=true 3 | -------------------------------------------------------------------------------- /src/declarative-agent/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | }, 11 | "extensionBundle": { 12 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 13 | "version": "[4.*, 5.0.0)" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/declarative-agent/http/treyResearchAPI.http: -------------------------------------------------------------------------------- 1 | @base_url = http://localhost:7071/api 2 | 3 | ########## /api/me - working with the Copilot user ########## 4 | 5 | ### Get my consultant and project information 6 | {{base_url}}/me 7 | 8 | ### Charge time to a project 9 | POST {{base_url}}/me/chargeTime 10 | Content-Type: application/json 11 | 12 | { 13 | "projectName": "woodgrove", 14 | "hours": 3 15 | } 16 | 17 | 18 | ########## /api/consultants - working with consultants ########## 19 | 20 | ### Get all consultants 21 | {{base_url}}/consultants 22 | 23 | ### Get consultant by id 24 | {{base_url}}/consultants/1 25 | 26 | ### Get consulatnt by name 27 | {{base_url}}/consultants/?consultantName=Sanjay 28 | 29 | ### Get consultants by project 30 | {{base_url}}/consultants/?projectName=woodgrove 31 | 32 | ### Get consultants by skill 33 | {{base_url}}/consultants/?skill=javascript 34 | 35 | ### Get consultants by certification 36 | {{base_url}}/consultants/?certification=azure 37 | 38 | ### Get consultants by role 39 | {{base_url}}/consultants/?role=developer 40 | 41 | ### Get consultants by hours available this month 42 | {{base_url}}/consultants/?hoursAvailable=100 43 | 44 | 45 | ########## /api/projects - working with projects ########## 46 | 47 | ### Get all projects 48 | {{base_url}}/projects 49 | 50 | ### Get project by id 51 | {{base_url}}/projects/1 52 | 53 | ### Get project by project or client name 54 | {{base_url}}/projects/?projectName=supply 55 | 56 | ### Get project by consultant name 57 | {{base_url}}/projects/?consultantName=dominique 58 | 59 | ### Add consultant to project 60 | POST {{base_url}}/projects/assignConsultant 61 | Content-Type: application/json 62 | 63 | { 64 | "projectName": "contoso", 65 | "consultantName": "sanjay", 66 | "role": "architect", 67 | "forecast": 30 68 | } 69 | 70 | -------------------------------------------------------------------------------- /src/declarative-agent/infra/azure.bicep: -------------------------------------------------------------------------------- 1 | @maxLength(20) 2 | @minLength(4) 3 | param resourceBaseName string 4 | param functionAppSKU string 5 | param functionStorageSKU string 6 | 7 | param location string = resourceGroup().location 8 | param serverfarmsName string = resourceBaseName 9 | param functionAppName string = resourceBaseName 10 | param functionStorageName string = '${resourceBaseName}api' 11 | 12 | // Azure Storage is required when creating Azure Functions instance 13 | resource functionStorage 'Microsoft.Storage/storageAccounts@2021-06-01' = { 14 | name: functionStorageName 15 | kind: 'StorageV2' 16 | location: location 17 | sku: { 18 | name: functionStorageSKU// You can follow https://aka.ms/teamsfx-bicep-add-param-tutorial to add functionStorageSKUproperty to provisionParameters to override the default value "Standard_LRS". 19 | } 20 | } 21 | 22 | // Storage account for database table storage 23 | resource storageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' = { 24 | name: resourceBaseName 25 | location: location 26 | kind: 'StorageV2' 27 | sku: { 28 | name: 'Standard_LRS' 29 | } 30 | } 31 | var storageAccountConnectionString = 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}' 32 | 33 | // Compute resources for Azure Functions 34 | resource serverfarms 'Microsoft.Web/serverfarms@2021-02-01' = { 35 | name: serverfarmsName 36 | location: location 37 | sku: { 38 | 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". 39 | } 40 | properties: {} 41 | } 42 | 43 | // Azure Functions that hosts your function code 44 | resource functionApp 'Microsoft.Web/sites@2021-02-01' = { 45 | name: functionAppName 46 | kind: 'functionapp' 47 | location: location 48 | properties: { 49 | serverFarmId: serverfarms.id 50 | httpsOnly: true 51 | siteConfig: { 52 | appSettings: [ 53 | { 54 | name: ' AzureWebJobsDashboard' 55 | value: 'DefaultEndpointsProtocol=https;AccountName=${functionStorage.name};AccountKey=${listKeys(functionStorage.id, functionStorage.apiVersion).keys[0].value};EndpointSuffix=${environment().suffixes.storage}' // Azure Functions internal setting 56 | } 57 | { 58 | name: 'AzureWebJobsStorage' 59 | value: 'DefaultEndpointsProtocol=https;AccountName=${functionStorage.name};AccountKey=${listKeys(functionStorage.id, functionStorage.apiVersion).keys[0].value};EndpointSuffix=${environment().suffixes.storage}' // Azure Functions internal setting 60 | } 61 | { 62 | name: 'FUNCTIONS_EXTENSION_VERSION' 63 | value: '~4' // Use Azure Functions runtime v4 64 | } 65 | { 66 | name: 'FUNCTIONS_WORKER_RUNTIME' 67 | value: 'node' // Set runtime to NodeJS 68 | } 69 | { 70 | name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING' 71 | value: 'DefaultEndpointsProtocol=https;AccountName=${functionStorage.name};AccountKey=${listKeys(functionStorage.id, functionStorage.apiVersion).keys[0].value};EndpointSuffix=${environment().suffixes.storage}' // Azure Functions internal setting 72 | } 73 | { 74 | name: 'WEBSITE_RUN_FROM_PACKAGE' 75 | value: '1' // Run Azure Functions from a package file 76 | } 77 | { 78 | name: 'WEBSITE_NODE_DEFAULT_VERSION' 79 | value: '~18' // Set NodeJS version to 18.x 80 | } 81 | { 82 | name: 'STORAGE_ACCOUNT_CONNECTION_STRING' 83 | value: storageAccountConnectionString 84 | } 85 | ] 86 | ftpsState: 'FtpsOnly' 87 | } 88 | } 89 | } 90 | var apiEndpoint = 'https://${functionApp.properties.defaultHostName}' 91 | 92 | 93 | // The output will be persisted in .env.{envName}. Visit https://aka.ms/teamsfx-actions/arm-deploy for more details. 94 | output API_FUNCTION_ENDPOINT string = apiEndpoint 95 | output API_FUNCTION_RESOURCE_ID string = functionApp.id 96 | output OPENAPI_SERVER_URL string = apiEndpoint 97 | output SECRET_STORAGE_ACCOUNT_CONNECTION_STRING string = storageAccountConnectionString 98 | -------------------------------------------------------------------------------- /src/declarative-agent/infra/azure.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "resourceBaseName": { 6 | "value": "sme${{RESOURCE_SUFFIX}}" 7 | }, 8 | "functionAppSKU": { 9 | "value": "Y1" 10 | }, 11 | "functionStorageSKU": { 12 | "value": "Standard_LRS" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/declarative-agent/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "FUNCTIONS_WORKER_RUNTIME": "node" 5 | } 6 | } -------------------------------------------------------------------------------- /src/declarative-agent/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "treyresearch", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "dev:teamsfx": "env-cmd --silent -f .localConfigs npm run dev", 6 | "dev": "func start --typescript --language-worker=\"--inspect=9229\" --port \"7071\" --cors \"*\"", 7 | "build": "tsc", 8 | "watch:teamsfx": "tsc --watch", 9 | "watch": "tsc -w", 10 | "prestart": "npm run build", 11 | "start": "npx func start", 12 | "test": "echo \"Error: no test specified\" && exit 1", 13 | "reset-local-db": "node ./scripts/db-setup.js UseDevelopmentStorage=true --reset", 14 | "storage": "azurite --silent --location ./_storage_emulator --debug ./_storage_emulator/debug.log" 15 | }, 16 | "dependencies": { 17 | "@azure/functions": "^4.3.0", 18 | "@azure/data-tables": "^13.2.2", 19 | "azurite": "^3.26.0" 20 | }, 21 | "devDependencies": { 22 | "env-cmd": "^10.1.0", 23 | "@types/node": "^18.11.9", 24 | "typescript": "^4.1.6" 25 | }, 26 | "main": "dist/src/functions/*.js" 27 | } 28 | -------------------------------------------------------------------------------- /src/declarative-agent/sampleDocs/Bellows College MSA.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/src/declarative-agent/sampleDocs/Bellows College MSA.docx -------------------------------------------------------------------------------- /src/declarative-agent/sampleDocs/Bellows College NDA.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/src/declarative-agent/sampleDocs/Bellows College NDA.docx -------------------------------------------------------------------------------- /src/declarative-agent/sampleDocs/Bellows College SOW - Network security review.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/src/declarative-agent/sampleDocs/Bellows College SOW - Network security review.docx -------------------------------------------------------------------------------- /src/declarative-agent/sampleDocs/My Hours.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/src/declarative-agent/sampleDocs/My Hours.xlsx -------------------------------------------------------------------------------- /src/declarative-agent/sampleDocs/Woodgrove MSA.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/src/declarative-agent/sampleDocs/Woodgrove MSA.docx -------------------------------------------------------------------------------- /src/declarative-agent/sampleDocs/Woodgrove NDA.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/src/declarative-agent/sampleDocs/Woodgrove NDA.docx -------------------------------------------------------------------------------- /src/declarative-agent/sampleDocs/Woodgrove SOW - Financial data plugin for Microsoft Copilot.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/aitour-customizing-microsoft-copilot/e72876068fb78afa04a4a126c31dfe66e20da8a8/src/declarative-agent/sampleDocs/Woodgrove SOW - Financial data plugin for Microsoft Copilot.docx -------------------------------------------------------------------------------- /src/declarative-agent/scripts/db-setup.js: -------------------------------------------------------------------------------- 1 | const { TableClient, TableServiceClient } = require("@azure/data-tables"); 2 | const { randomUUID } = require("crypto"); 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | 6 | const TABLE_NAMES = [ "Project", "Consultant", "Assignment" ]; 7 | 8 | (async () => { 9 | 10 | const connectionString = process.argv[2] ? process.argv[2] : "UseDevelopmentStorage=true"; 11 | const reset = process.argv[3] === "--reset" || process.argv[3] === "-r" ? true : false; 12 | 13 | const tableServiceClient = TableServiceClient.fromConnectionString(connectionString); 14 | 15 | // Function returns an array of table names in the storage account 16 | async function getTables(tableServiceClient) { 17 | let tables = []; 18 | for await (const table of tableServiceClient.listTables()) { 19 | tables.push(table.name) 20 | } 21 | return tables; 22 | } 23 | 24 | // If reset is true, delete all tables 25 | if (reset) { 26 | const tables = await getTables(tableServiceClient); 27 | tables.forEach(async table => { 28 | const tableClient = TableClient.fromConnectionString(connectionString, table); 29 | console.log(`Deleting table: ${table}`); 30 | await tableClient.deleteTable(); 31 | }); 32 | let tablesExist = true; 33 | while (tablesExist) { 34 | console.log("Waiting for tables to be deleted..."); 35 | const tables = await getTables(tableServiceClient); 36 | if (tables.length === 0) { 37 | tablesExist = false; 38 | console.log("All tables deleted."); 39 | } 40 | await new Promise(resolve => setTimeout(resolve, 1000)); 41 | } 42 | } 43 | 44 | // Create and populate tables 45 | TABLE_NAMES.forEach(async (tableName, index) => { 46 | 47 | // Skip if table already exists 48 | const tables = await getTables(tableServiceClient); 49 | if (tables.includes(tableName)) { 50 | console.log(`Table ${tableName} already exists, skipping...`); 51 | return; 52 | } 53 | 54 | // Create table if needed 55 | console.log(`Creating table: ${tableName}`); 56 | let tableCreated = false; 57 | while (!tableCreated) { 58 | try { 59 | await tableServiceClient.createTable(tableName); 60 | tableCreated = true; 61 | } catch (err) { 62 | if (err.statusCode === 409) { 63 | console.log('Table is marked for deletion, retrying in 5 seconds...'); 64 | await new Promise(resolve => setTimeout(resolve, 5000)); 65 | } else { 66 | throw err; 67 | } 68 | } 69 | } 70 | 71 | // Add entities to table 72 | const tableClient = TableClient.fromConnectionString(connectionString, tableName); 73 | const jsonString = fs.readFileSync(path.resolve(__dirname, "db", `${tableName}.json`), "utf8"); 74 | const entities = JSON.parse(jsonString); 75 | 76 | for (const entity of entities["rows"]) { 77 | const rowKey = entity["id"].toString() || randomUUID(); 78 | // Convert any nested objects to JSON strings 79 | for (const key in entity) { 80 | const valueType = Object.prototype.toString.call(entity[key]); 81 | if (valueType === "[object Object]" || valueType === "[object Array]") { 82 | entity[key] = JSON.stringify(entity[key]); 83 | } 84 | } 85 | await tableClient.createEntity({ 86 | partitionKey: tableName, 87 | rowKey, 88 | ...entity 89 | }); 90 | 91 | console.log(`Added entity to ${tableName} with key ${rowKey}`); 92 | 93 | } 94 | }); 95 | 96 | })(); -------------------------------------------------------------------------------- /src/declarative-agent/scripts/db/Assignment.json: -------------------------------------------------------------------------------- 1 | { 2 | "rows": [ 3 | { 4 | "id": "10,1", 5 | "projectId": "10", 6 | "consultantId": "1", 7 | "role": "Architect", 8 | "billable": true, 9 | "rate": 100, 10 | "forecast": [ 11 | { 12 | "month": 3, 13 | "year": 2024, 14 | "hours": 23 15 | }, 16 | { 17 | "month": 4, 18 | "year": 2024, 19 | "hours": 80 20 | }, 21 | { 22 | "month": 5, 23 | "year": 2024, 24 | "hours": 100 25 | }, 26 | { 27 | "month": 6, 28 | "year": 2024, 29 | "hours": 1 30 | }, 31 | { 32 | "month": 7, 33 | "year": 2024, 34 | "hours": 120 35 | }, 36 | { 37 | "month": 8, 38 | "year": 2024, 39 | "hours": 120 40 | }, 41 | { 42 | "month": 9, 43 | "year": 2024, 44 | "hours": 120 45 | }, 46 | { 47 | "month": 10, 48 | "year": 2024, 49 | "hours": 120 50 | } 51 | ], 52 | "delivered": [ ] 53 | }, 54 | { 55 | "id": "10,2", 56 | "projectId": "10", 57 | "consultantId": "2", 58 | "role": "Designer", 59 | "billable": true, 60 | "rate": 100, 61 | "forecast": [ 62 | { 63 | "month": 3, 64 | "year": 2024, 65 | "hours": 100 66 | }, 67 | { 68 | "month": 4, 69 | "year": 2024, 70 | "hours": 20 71 | }, 72 | { 73 | "month": 5, 74 | "year": 2024, 75 | "hours": 20 76 | }, 77 | { 78 | "month": 6, 79 | "year": 2024, 80 | "hours": 20 81 | }, 82 | { 83 | "month": 7, 84 | "year": 2024, 85 | "hours": 20 86 | }, 87 | { 88 | "month": 8, 89 | "year": 2024, 90 | "hours": 10 91 | }, 92 | { 93 | "month": 9, 94 | "year": 2024, 95 | "hours": 10 96 | }, 97 | { 98 | "month": 10, 99 | "year": 2024, 100 | "hours": 10 101 | } 102 | ], 103 | "delivered": [ ] 104 | }, 105 | { 106 | "id": "4,1", 107 | "projectId": "4", 108 | "consultantId": "1", 109 | "role": "Developer", 110 | "billable": true, 111 | "rate": 100, 112 | "forecast": [ 113 | { 114 | "month": 3, 115 | "year": 2024, 116 | "hours": 40 117 | }, 118 | { 119 | "month": 4, 120 | "year": 2024, 121 | "hours": 40 122 | }, 123 | { 124 | "month": 5, 125 | "year": 2024, 126 | "hours": 40 127 | }, 128 | { 129 | "month": 6, 130 | "year": 2024, 131 | "hours": 40 132 | }, 133 | { 134 | "month": 7, 135 | "year": 2024, 136 | "hours": 40 137 | }, 138 | { 139 | "month": 8, 140 | "year": 2024, 141 | "hours": 40 142 | }, 143 | { 144 | "month": 9, 145 | "year": 2024, 146 | "hours": 40 147 | }, 148 | { 149 | "month": 10, 150 | "year": 2024, 151 | "hours": 40 152 | } 153 | ], 154 | "delivered": [ ] 155 | } 156 | ] 157 | } 158 | 159 | -------------------------------------------------------------------------------- /src/declarative-agent/scripts/db/Consultant.json: -------------------------------------------------------------------------------- 1 | { 2 | "rows": [ 3 | { 4 | "id": "1", 5 | "name": "Avery Howard", 6 | "email": "avery@treyresearch.com", 7 | "phone": "1-555-456-7890", 8 | "consultantPhotoUrl": "https://bobgerman.github.io/fictitiousAiGenerated/Avery.jpg", 9 | "location": { 10 | "street": "5 Wayside Rd.", 11 | "city": "Burlington", 12 | "state": "MA", 13 | "country": "USA", 14 | "postalCode": "01803", 15 | "latitude": 42.5048, 16 | "longitude": -71.1956 17 | }, 18 | "skills": [ "C#", "JavaScript", "TypeScript", "React", "Node.js" ], 19 | "certifications": [ "MCSADA", "Azure Developer Associate", "MCAAF", "Azure AI Fundamentals" ], 20 | "roles": [ "Project lead", "Developer", "Architect", "DevOps" ] 21 | }, 22 | { 23 | "id": "2", 24 | "name": "Dominique Dutertre", 25 | "email": "dominique@treyresearch.com", 26 | "phone": "1-555-567-7890", 27 | "consultantPhotoUrl": "https://bobgerman.github.io/fictitiousAiGenerated/Dominique.jpg", 28 | "location": { 29 | "street": "39 Quai du Président Roosevelt, Issy-les-Moulineaux", 30 | "city": "Paris", 31 | "state": "Île-de-France", 32 | "country": "France", 33 | "postalCode": "IdF 92130", 34 | "latitude": 48.8264, 35 | "longitude": 2.2675 36 | }, 37 | "skills": [ "Figma", "Adobe Creative Swuite", "Visual design" ], 38 | "certifications": [ ], 39 | "roles": [ "Designer", "Project lead" ] 40 | }, 41 | { 42 | "id": "3", 43 | "name": "Robin Zupanc", 44 | "email": "robin@treyresearch.com", 45 | "phone": "1-555-344-7890", 46 | "consultantPhotoUrl": "https://bobgerman.github.io/fictitiousAiGenerated/Robin.jpg", 47 | "location": { 48 | "street": "11 Times Square", 49 | "city": "New York", 50 | "state": "NY", 51 | "country": "USA", 52 | "postalCode": "10036", 53 | "latitude": 40.7580, 54 | "longitude": -73.9855 55 | }, 56 | "skills": [ "Python", "Java" ], 57 | "certifications": [ "MCSADA", "Azure Developer Associate", "MCAAF", "Azure AI Fundamentals", "AWS Certified Developer", "Google Professional Cloud Developer" ], 58 | "roles": [ "Project lead", "Developer", "Architect, DevOps" ] 59 | }, 60 | { 61 | "id": "4", 62 | "name": "Sanjay Puranik", 63 | "email": "sanjay@treyresearch.com", 64 | "phone": "1-555-562-7890", 65 | "consultantPhotoUrl": "https://bobgerman.github.io/fictitiousAiGenerated/Sanjay.jpg", 66 | "location": { 67 | "street": "2 Kingdom St.", 68 | "city": "Paddington, London", 69 | "state": "EN", 70 | "country": "GBR", 71 | "postalCode": "W2 6BD", 72 | "latitude": 51.5194, 73 | "longitude": -0.1781 74 | }, 75 | "skills": [ "TypeScript", "JavaScript", "Angular", "React", "Node.js" ], 76 | "certifications": [ "MCPPSAE", "Power Platform Solution Architect Expert", "MCAF", "Azure Fundamentals" ], 77 | "roles": [ "Project lead", "Developer" ] 78 | }, 79 | { 80 | "id": "5", 81 | "name": "Lois Wyn", 82 | "email": "lois@treyresearch.com", 83 | "phone": "1-555-664-7890", 84 | "consultantPhotoUrl": "https://bobgerman.github.io/fictitiousAiGenerated/Lois.jpg", 85 | "location": { 86 | "street": "4220 Duncan Ave., Suite 501", 87 | "city": "St. Louis", 88 | "state": "MO", 89 | "country": "USA", 90 | "postalCode": "63110", 91 | "latitude": 38.6283, 92 | "longitude": -90.2545 93 | }, 94 | "skills": [ "Project management", "Agile" ], 95 | "certifications": [ "Project Management Professional", "PMP", "Agile Leader" ], 96 | "roles": [ "Project lead" ] 97 | } 98 | ] 99 | } -------------------------------------------------------------------------------- /src/declarative-agent/src/functions/consultants.ts: -------------------------------------------------------------------------------- 1 | /* This code sample provides a starter kit to implement server side logic for your Teams App in TypeScript, 2 | * refer to https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference for complete Azure Functions 3 | * developer guide. 4 | */ 5 | 6 | import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions"; 7 | import ConsultantApiService from "../services/ConsultantApiService"; 8 | import { ApiConsultant, ErrorResult } from "../model/apiModel"; 9 | import { cleanUpParameter } from "../services/Utilities"; 10 | import IdentityService from "../services/IdentityService"; 11 | 12 | /** 13 | * This function handles the HTTP request and returns the consultant information. 14 | * 15 | * @param {HttpRequest} req - The HTTP request. 16 | * @param {InvocationContext} context - The Azure Functions context object. 17 | * @returns {Promise} - A promise that resolves with the HTTP response containing the consultant information. 18 | */ 19 | 20 | // Define a Response interface. 21 | interface Response extends HttpResponseInit { 22 | status: number; 23 | jsonBody: { 24 | results: ApiConsultant[] | ErrorResult; 25 | }; 26 | } 27 | export async function consultants( 28 | req: HttpRequest, 29 | context: InvocationContext 30 | ): Promise { 31 | context.log("HTTP trigger function consultants processed a request."); 32 | 33 | // Initialize response. 34 | const res: Response = { 35 | status: 200, 36 | jsonBody: { 37 | results: [], 38 | }, 39 | }; 40 | try { 41 | // Will throw an exception if the request is not valid 42 | await IdentityService.validateRequest(req); 43 | 44 | // Get the input parameters 45 | let consultantName = req.query.get("consultantName")?.toString().toLowerCase() || ""; 46 | let projectName = req.query.get("projectName")?.toString().toLowerCase() || ""; 47 | let skill = req.query.get("skill")?.toString().toLowerCase() || ""; 48 | let certification = req.query.get("certification")?.toString().toLowerCase() || ""; 49 | let role = req.query.get("role")?.toString().toLowerCase() || ""; 50 | let hoursAvailable = req.query.get("hoursAvailable")?.toString().toLowerCase() || ""; 51 | 52 | const id = req.params?.id?.toLowerCase(); 53 | 54 | if (id) { 55 | console.log(`➡️ GET /api/consultants/${id}: request for consultant ${id}`); 56 | const result = await ConsultantApiService.getApiConsultantById(id); 57 | res.jsonBody.results = [result]; 58 | console.log(` ✅ GET /api/consultants/${id}: response status 1 consultant returned`); 59 | return res; 60 | } 61 | 62 | console.log(`➡️ GET /api/consultants: request for consultantName=${consultantName}, projectName=${projectName}, skill=${skill}, certification=${certification}, role=${role}, hoursAvailable=${hoursAvailable}`); 63 | 64 | // *** Tweak parameters for the AI *** 65 | consultantName = cleanUpParameter("consultantName", consultantName); 66 | projectName = cleanUpParameter("projectName", projectName); 67 | skill = cleanUpParameter("skill", skill); 68 | certification = cleanUpParameter("certification", certification); 69 | role = cleanUpParameter("role", role); 70 | hoursAvailable = cleanUpParameter("hoursAvailable", hoursAvailable); 71 | 72 | const result = await ConsultantApiService.getApiConsultants( 73 | consultantName, projectName, skill, certification, role, hoursAvailable 74 | ); 75 | res.jsonBody.results = result; 76 | console.log(` ✅ GET /api/consultants: response status ${res.status}; ${result.length} consultants returned`); 77 | return res; 78 | 79 | } catch (error) { 80 | const status = error.status || error.response?.status || 500; 81 | console.log(` ⛔ Returning error status code ${status}: ${error.message}`); 82 | 83 | res.status = status; 84 | res.jsonBody.results = { 85 | status: status, 86 | message: error.message 87 | }; 88 | return res; 89 | } 90 | } 91 | 92 | app.http("consultants", { 93 | methods: ["GET"], 94 | authLevel: "anonymous", 95 | route: "consultants/{*id}", 96 | handler: consultants, 97 | }); 98 | -------------------------------------------------------------------------------- /src/declarative-agent/src/functions/me.ts: -------------------------------------------------------------------------------- 1 | /* This code sample provides a starter kit to implement server side logic for your Teams App in TypeScript, 2 | * refer to https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference for complete Azure Functions 3 | * developer guide. 4 | */ 5 | 6 | import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions"; 7 | import ConsultantApiService from "../services/ConsultantApiService"; 8 | import { ApiConsultant, ApiChargeTimeResponse, ErrorResult } from "../model/apiModel"; 9 | import { HttpError, cleanUpParameter } from "../services/Utilities"; 10 | import IdentityService from "../services/IdentityService"; 11 | /** 12 | * This function handles the HTTP request and returns my information. 13 | * 14 | * @param {HttpRequest} req - The HTTP request. 15 | * @param {InvocationContext} context - The Azure Functions context object. 16 | * @returns {Promise} - A promise that resolves with the HTTP response containing my information. 17 | */ 18 | 19 | // Define a Response interface. 20 | interface Response extends HttpResponseInit { 21 | status: number; 22 | jsonBody: { 23 | results: ApiConsultant[] | ApiChargeTimeResponse | ErrorResult; 24 | }; 25 | } 26 | export async function me( 27 | req: HttpRequest, 28 | context: InvocationContext 29 | ): Promise { 30 | context.log("HTTP trigger function me processed a request."); 31 | 32 | // Initialize response. 33 | const res: Response = { 34 | status: 200, 35 | jsonBody: { 36 | results: [], 37 | }, 38 | }; 39 | 40 | try { 41 | const me = await IdentityService.validateRequest(req); 42 | const command = req.params.command?.toLowerCase(); 43 | let body=null; 44 | switch (req.method) { 45 | case "GET": { 46 | 47 | if (command) { 48 | throw new HttpError(400, `Invalid command: ${command}`); 49 | } 50 | 51 | console.log(`➡️ GET /api/me request`); 52 | 53 | const result = [ me ]; 54 | res.jsonBody.results = result; 55 | console.log(` ✅ GET /me response status ${res.status}; ${result.length} consultants returned`); 56 | return res; 57 | } 58 | case "POST": { 59 | const me = await IdentityService.validateRequest(req); 60 | switch (command) { 61 | case "chargetime": { 62 | try { 63 | const bd = await req.text(); 64 | body = JSON.parse(bd); 65 | } catch (error) { 66 | throw new HttpError(400, `No body to process this request.`); 67 | } 68 | if (body) { 69 | 70 | const projectName = cleanUpParameter("projectName", body["projectName"]); 71 | if (!projectName) { 72 | throw new HttpError(400, `Missing project name`); 73 | } 74 | const hours = body["hours"]; 75 | if (!hours) { 76 | throw new HttpError(400, `Missing hours`); 77 | } 78 | if (typeof hours !== 'number' || hours < 0 || hours > 24) { 79 | throw new HttpError(400, `Invalid hours: ${hours}`); 80 | } 81 | 82 | console.log(`➡️ POST /api/me/chargetime request for project ${projectName}, hours ${hours}`); 83 | const result = await ConsultantApiService.chargeTimeToProject (projectName, me.id, hours); 84 | 85 | res.jsonBody.results = { 86 | status: 200, 87 | clientName: result.clientName, 88 | projectName: result.projectName, 89 | remainingForecast: result.remainingForecast, 90 | message: result.message 91 | }; 92 | console.log(` ✅ POST /api/me/chargetime response status ${res.status}; ${result.message}`); 93 | } else { 94 | throw new HttpError(400, `Missing request body`); 95 | } 96 | return res; 97 | } 98 | default: { 99 | throw new HttpError(400, `Invalid command: ${command}`); 100 | } 101 | } 102 | } 103 | default: 104 | throw new HttpError(405, `Method not allowed: ${req.method}`); 105 | } 106 | 107 | } catch (error) { 108 | 109 | const status = error.status || error.response?.status || 500; 110 | console.log(` ⛔ Returning error status code ${status}: ${error.message}`); 111 | 112 | res.status = status; 113 | res.jsonBody.results = { 114 | status: status, 115 | message: error.message 116 | }; 117 | return res; 118 | } 119 | 120 | } 121 | 122 | app.http("me", { 123 | methods: ["GET","POST"], 124 | authLevel: "anonymous", 125 | route: "me/{*command}", 126 | handler: me, 127 | }); 128 | -------------------------------------------------------------------------------- /src/declarative-agent/src/functions/projects.ts: -------------------------------------------------------------------------------- 1 | /* This code sample provides a starter kit to implement server side logic for your Teams App in TypeScript, 2 | * refer to https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference for complete Azure Functions 3 | * developer guide. 4 | */ 5 | 6 | import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions"; 7 | import ProjectApiService from "../services/ProjectApiService"; 8 | import { ApiProject, ApiAddConsultantToProjectResponse, ErrorResult } from "../model/apiModel"; 9 | import { HttpError, cleanUpParameter } from "../services/Utilities"; 10 | import IdentityService from "../services/IdentityService"; 11 | 12 | /** 13 | * This function handles the HTTP request and returns the project information. 14 | * 15 | * @param {HttpRequest} req - The HTTP request. 16 | * @param {InvocationContext} context - The Azure Functions context object. 17 | * @returns {Promise} - A promise that resolves with the HTTP response containing the project information. 18 | */ 19 | 20 | // Define a Response interface. 21 | interface Response extends HttpResponseInit { 22 | status: number; 23 | jsonBody: { 24 | results: ApiProject[] | ApiAddConsultantToProjectResponse | ErrorResult; 25 | }; 26 | } 27 | export async function projects( 28 | req: HttpRequest, 29 | context: InvocationContext 30 | ): Promise { 31 | context.log("HTTP trigger function projects processed a request."); 32 | // Initialize response. 33 | const res: Response = { 34 | status: 200, 35 | jsonBody: { 36 | results: [], 37 | }, 38 | }; 39 | 40 | try { 41 | 42 | // Will throw an exception if the request is not valid 43 | const userInfo = await IdentityService.validateRequest(req); 44 | 45 | const id = req.params?.id?.toLowerCase(); 46 | let body = null; 47 | switch (req.method) { 48 | case "GET": { 49 | 50 | let projectName = req.query.get("projectName")?.toString().toLowerCase() || ""; 51 | let consultantName = req.query.get("consultantName")?.toString().toLowerCase() || ""; 52 | 53 | console.log(`➡️ GET /api/projects: request for projectName=${projectName}, consultantName=${consultantName}, id=${id}`); 54 | 55 | projectName = cleanUpParameter("projectName", projectName); 56 | consultantName = cleanUpParameter("consultantName", consultantName); 57 | 58 | if (id) { 59 | const result = await ProjectApiService.getApiProjectById(id); 60 | res.jsonBody.results = [result]; 61 | console.log(` ✅ GET /api/projects: response status ${res.status}; 1 projects returned`); 62 | return res; 63 | } 64 | 65 | // Use current user if the project name is user_profile 66 | if (projectName.includes('user_profile')) { 67 | const result = await ProjectApiService.getApiProjects("", userInfo.name); 68 | res.jsonBody.results = result; 69 | console.log(` ✅ GET /api/projects for current user response status ${res.status}; ${result.length} projects returned`); 70 | return res; 71 | } 72 | 73 | const result = await ProjectApiService.getApiProjects(projectName, consultantName); 74 | res.jsonBody.results = result; 75 | console.log(` ✅ GET /api/projects: response status ${res.status}; ${result.length} projects returned`); 76 | return res; 77 | } 78 | case "POST": { 79 | switch (id.toLocaleLowerCase()) { 80 | case "assignconsultant": { 81 | try { 82 | const bd = await req.text(); 83 | body = JSON.parse(bd); 84 | } catch (error) { 85 | throw new HttpError(400, `No body to process this request.`); 86 | } 87 | if (body) { 88 | const projectName = cleanUpParameter("projectName", body["projectName"]); 89 | if (!projectName) { 90 | throw new HttpError(400, `Missing project name`); 91 | } 92 | const consultantName = cleanUpParameter("consultantName", body["consultantName"]?.toString() || ""); 93 | if (!consultantName) { 94 | throw new HttpError(400, `Missing consultant name`); 95 | } 96 | const role = cleanUpParameter("Role", body["role"]); 97 | if (!role) { 98 | throw new HttpError(400, `Missing role`); 99 | } 100 | let forecast = body["forecast"]; 101 | if (!forecast) { 102 | forecast = 0; 103 | //throw new HttpError(400, `Missing forecast this month`); 104 | } 105 | console.log(`➡️ POST /api/projects: assignconsultant request, projectName=${projectName}, consultantName=${consultantName}, role=${role}, forecast=${forecast}`); 106 | const result = await ProjectApiService.addConsultantToProject 107 | (projectName, consultantName, role, forecast); 108 | 109 | res.jsonBody.results = { 110 | status: 200, 111 | clientName: result.clientName, 112 | projectName: result.projectName, 113 | consultantName: result.consultantName, 114 | remainingForecast: result.remainingForecast, 115 | message: result.message 116 | }; 117 | 118 | console.log(` ✅ POST /api/projects: response status ${res.status} - ${result.message}`); 119 | } else { 120 | throw new HttpError(400, `Missing request body`); 121 | } 122 | return res; 123 | } 124 | default: { 125 | throw new HttpError(400, `Invalid command: ${id}`); 126 | } 127 | } 128 | 129 | } 130 | default: { 131 | throw new Error(`Method not allowed: ${req.method}`); 132 | } 133 | } 134 | 135 | } catch (error) { 136 | 137 | const status = error.status || error.response?.status || 500; 138 | console.log(` ⛔ Returning error status code ${status}: ${error.message}`); 139 | 140 | res.status = status; 141 | res.jsonBody.results = { 142 | status: status, 143 | message: error.message 144 | }; 145 | return res; 146 | } 147 | } 148 | 149 | app.http("projects", { 150 | methods: ["GET", "POST"], 151 | authLevel: "anonymous", 152 | route: "projects/{*id}", 153 | handler: projects, 154 | }); 155 | -------------------------------------------------------------------------------- /src/declarative-agent/src/model/apiModel.ts: -------------------------------------------------------------------------------- 1 | import { Project, Consultant, Location } from './baseModel'; 2 | 3 | //#region GET requests for /projects -------------------- 4 | 5 | export interface ApiProjectAssignment { 6 | consultantName: string; 7 | consultantLocation: Location; 8 | role: string; 9 | forecastThisMonth: number; 10 | forecastNextMonth: number; 11 | deliveredLastMonth: number; 12 | deliveredThisMonth: number; 13 | } 14 | 15 | // Returned by all /api/projects GET requests 16 | export interface ApiProject extends Project { 17 | consultants: ApiProjectAssignment[]; 18 | forecastThisMonth: number; 19 | forecastNextMonth: number; 20 | deliveredLastMonth: number; 21 | deliveredThisMonth: number; 22 | } 23 | //#endregion 24 | 25 | //#region GET requests for /me and /consultants --- 26 | 27 | // Information about a project that a consultant is assigned to 28 | export interface ApiConsultantAssignment { 29 | projectName: string; 30 | projectDescription: string; 31 | projectLocation: Location; 32 | clientName: string; 33 | clientContact: string; 34 | clientEmail: string; 35 | role: string; 36 | forecastThisMonth: number; 37 | forecastNextMonth: number; 38 | deliveredLastMonth: number; 39 | deliveredThisMonth: number; 40 | } 41 | 42 | // Returned by all /api/consultants GET requests 43 | export interface ApiConsultant extends Consultant { 44 | projects: ApiConsultantAssignment[]; 45 | forecastThisMonth: number; 46 | forecastNextMonth: number; 47 | deliveredLastMonth: number; 48 | deliveredThisMonth: number; 49 | } 50 | //#endregion 51 | 52 | //#region POST request to /api/me/chargeTime --- 53 | export interface ApiChargeTimeRequest { 54 | projectName: string; 55 | hours: number; 56 | } 57 | export interface ApiChargeTimeResponse { 58 | clientName: string; 59 | projectName: string; 60 | remainingForecast: number; 61 | message: string; 62 | } 63 | //#endregion 64 | 65 | //#region POST request to /api/projects/assignConsultant --- 66 | export interface ApiAddConsultantToProjectRequest { 67 | projectName: string; 68 | consultantName: string; 69 | role: string; 70 | hours: number; 71 | } 72 | export interface ApiAddConsultantToProjectResponse { 73 | clientName: string; 74 | projectName: string; 75 | consultantName: string; 76 | remainingForecast: number; 77 | message: string; 78 | } 79 | //#endregion 80 | 81 | export interface ErrorResult { 82 | status: number; 83 | message: string; 84 | } 85 | -------------------------------------------------------------------------------- /src/declarative-agent/src/model/baseModel.ts: -------------------------------------------------------------------------------- 1 | export interface Location { 2 | street: string; 3 | city: string; 4 | state: string; 5 | country: string; 6 | postalCode: string; 7 | latitude: number; 8 | longitude: number; 9 | } 10 | 11 | export interface HoursEntry { 12 | month: number; 13 | year: number; 14 | hours: number; 15 | } 16 | 17 | export interface Project { 18 | id: string; 19 | name: string; 20 | description: string; 21 | clientName: string; 22 | clientContact: string; 23 | clientEmail: string; 24 | location: Location; 25 | mapUrl: string; 26 | } 27 | 28 | export interface Consultant { 29 | id: string; 30 | name: string; 31 | email: string; 32 | phone: string; 33 | consultantPhotoUrl: string; 34 | location: Location; 35 | skills: string[]; 36 | certifications: string[]; 37 | roles: string[]; 38 | } 39 | 40 | export interface Assignment { 41 | id: string; // The assignment ID is "projectid,consultantid" 42 | projectId: string; 43 | consultantId: string; 44 | role: string; 45 | billable: boolean; 46 | rate: number; 47 | forecast: HoursEntry []; 48 | delivered: HoursEntry[]; 49 | } 50 | -------------------------------------------------------------------------------- /src/declarative-agent/src/model/dbModel.ts: -------------------------------------------------------------------------------- 1 | import { TableEntity } from "@azure/data-tables"; 2 | import { Project, Consultant, Assignment } from "./baseModel"; 3 | 4 | export interface DbEntity extends TableEntity { 5 | etag: string; 6 | partitionKey: string; 7 | rowKey: string; 8 | timestamp: Date; 9 | } 10 | 11 | export interface DbProject extends DbEntity, Project { } 12 | 13 | export interface DbConsultant extends DbEntity, Consultant { } 14 | 15 | export interface DbAssignment extends DbEntity, Assignment { } -------------------------------------------------------------------------------- /src/declarative-agent/src/services/AssignmentDbService.ts: -------------------------------------------------------------------------------- 1 | import DbService from './DbService'; 2 | import { DbAssignment } from '../model/dbModel'; 3 | import { Assignment } from '../model/baseModel'; 4 | import { HttpError } from './Utilities'; 5 | 6 | const TABLE_NAME = "Assignment"; 7 | 8 | class AssignmentDbService { 9 | 10 | // NOTE: Assignments are READ-WRITE so disable local caching 11 | private dbService = new DbService(false); 12 | 13 | async getAssignments(): Promise { 14 | const assignments = await this.dbService.getEntities(TABLE_NAME) as DbAssignment[]; 15 | const result = assignments.map((e) => this.convertDbAssignment(e)); 16 | return result; 17 | } 18 | 19 | async chargeHoursToProject(projectId: string, consultantId: string, month: number, year: number, hours: number): Promise { 20 | try { 21 | const dbAssignment = await this.dbService.getEntityByRowKey(TABLE_NAME, projectId + "," + consultantId) as DbAssignment; 22 | if (!dbAssignment) { 23 | throw new HttpError(404, "Assignment not found"); 24 | } 25 | // Add the hours delivered 26 | if (!dbAssignment.delivered) { 27 | dbAssignment.delivered = [{ month: month, year: year, hours: hours }]; 28 | } else { 29 | let a = dbAssignment.delivered.find(d => d.month === month && d.year === year); 30 | if (a) { 31 | a.hours += hours; 32 | } else { 33 | dbAssignment.delivered.push({ month, year, hours }); 34 | } 35 | } 36 | dbAssignment.delivered.sort((a, b) => a.year - b.year || a.month - b.month); 37 | 38 | // Subtract the hours from the forecast 39 | let remainingForecast = -hours; 40 | if (!dbAssignment.forecast) { 41 | dbAssignment.forecast = [{ month: month, year: year, hours: -hours }]; 42 | } else { 43 | let a = dbAssignment.forecast.find(d => d.month === month && d.year === year); 44 | if (a) { 45 | a.hours -= hours; 46 | remainingForecast = a.hours; 47 | } else { 48 | dbAssignment.forecast.push({ month: month, year: year, hours: -hours }); 49 | } 50 | } 51 | dbAssignment.forecast.sort((a, b) => a.year - b.year || a.month - b.month); 52 | 53 | await this.dbService.updateEntity(TABLE_NAME, dbAssignment) 54 | 55 | return remainingForecast; 56 | } catch (e) { 57 | throw new HttpError(404, "Assignment not found"); 58 | } 59 | } 60 | 61 | async addConsultantToProject(projectId: string, consultantId: string, role: string, hours: number): Promise { 62 | 63 | const month = new Date().getMonth() + 1; 64 | const year = new Date().getFullYear(); 65 | 66 | let dbAssignment = null; 67 | try { 68 | dbAssignment = await this.dbService.getEntityByRowKey(TABLE_NAME, projectId + "," + consultantId) as DbAssignment; 69 | } catch { } 70 | 71 | if (dbAssignment) { 72 | throw new HttpError(403, "Assignment already exists"); 73 | } 74 | 75 | try { 76 | const newAssignment: DbAssignment = { 77 | etag: "", 78 | partitionKey: TABLE_NAME, 79 | rowKey: projectId + "," + consultantId, 80 | timestamp: new Date(), 81 | id: projectId + "," + consultantId, 82 | projectId: projectId, 83 | consultantId: consultantId, 84 | role: role, 85 | billable: true, 86 | rate: 100, 87 | forecast: [{ month: month, year: year, hours: hours }], 88 | delivered: [] 89 | }; 90 | 91 | await this.dbService.createEntity(TABLE_NAME, newAssignment.id, newAssignment) 92 | 93 | return hours; 94 | } catch (e) { 95 | throw new HttpError(500, "Unable to add assignment"); 96 | } 97 | } 98 | 99 | private convertDbAssignment(dbAssignment: DbAssignment): Assignment { 100 | const result: Assignment = { 101 | id: dbAssignment.id, 102 | projectId: dbAssignment.projectId, 103 | consultantId: dbAssignment.consultantId, 104 | role: dbAssignment.role, 105 | billable: dbAssignment.billable, 106 | rate: dbAssignment.rate, 107 | forecast: dbAssignment.forecast, 108 | delivered: dbAssignment.delivered 109 | }; 110 | 111 | return result; 112 | } 113 | } 114 | 115 | export default new AssignmentDbService(); 116 | -------------------------------------------------------------------------------- /src/declarative-agent/src/services/ConsultantDbService.ts: -------------------------------------------------------------------------------- 1 | import DbService from './DbService'; 2 | import { DbConsultant } from '../model/dbModel'; 3 | import { Consultant } from '../model/baseModel'; 4 | 5 | const TABLE_NAME = "Consultant"; 6 | 7 | class ConsultantDbService { 8 | 9 | // NOTE: Consultants are READ ONLY in this demo app, so we are free to cache them in memory. 10 | private dbService = new DbService(true); 11 | 12 | async getConsultantById(id: string): Promise { 13 | const consultant = await this.dbService.getEntityByRowKey(TABLE_NAME, id) as DbConsultant; 14 | return consultant; 15 | } 16 | 17 | async getConsultants(): Promise { 18 | const consultants = await this.dbService.getEntities(TABLE_NAME) as DbConsultant[]; 19 | return consultants; 20 | } 21 | 22 | async createConsultant(consultant: Consultant): Promise { 23 | 24 | const newDbConsultant: DbConsultant = 25 | { 26 | ...consultant, 27 | etag: "", 28 | partitionKey: TABLE_NAME, 29 | rowKey: consultant.id, 30 | timestamp: new Date() 31 | }; 32 | await this.dbService.createEntity(TABLE_NAME, newDbConsultant.id, newDbConsultant) 33 | 34 | console.log (`Added new consultant ${newDbConsultant.name} (${newDbConsultant.id}) to the Consultant table`); 35 | return null; 36 | } 37 | 38 | } 39 | 40 | export default new ConsultantDbService(); 41 | -------------------------------------------------------------------------------- /src/declarative-agent/src/services/DbService.ts: -------------------------------------------------------------------------------- 1 | import { TableClient, TableEntity } from "@azure/data-tables"; 2 | import { HttpError } from './Utilities'; 3 | import { DbEntity } from '../model/dbModel'; 4 | 5 | export default class DbService { 6 | 7 | private storageAccountConnectionString = process.env.STORAGE_ACCOUNT_CONNECTION_STRING; 8 | private okToCacheLocally = false; 9 | private entityCache: DbEntity[] = []; 10 | 11 | constructor(okToCacheLocally: boolean) { 12 | if (!this.storageAccountConnectionString) { 13 | throw new Error("STORAGE_ACCOUNT_CONNECTION_STRING is not set"); 14 | } 15 | this.okToCacheLocally = okToCacheLocally; 16 | } 17 | 18 | async getEntityByRowKey(tableName: string, rowKey: string): Promise { 19 | if (!this.okToCacheLocally) { 20 | const tableClient = TableClient.fromConnectionString(this.storageAccountConnectionString, tableName); 21 | const result = this.expandPropertyValues(await tableClient.getEntity(tableName, rowKey) as DbEntityType); 22 | return result as DbEntity; 23 | } else { 24 | let result = await this.getEntities(tableName); 25 | result = result.filter((e) => { 26 | return e.rowKey === rowKey 27 | }); 28 | if (result.length === 0) { 29 | throw new HttpError(404, `Entity ${rowKey} not found`); 30 | } else { 31 | return result[0]; 32 | } 33 | } 34 | } 35 | 36 | async getEntities(tableName: string): Promise { 37 | 38 | if (!this.okToCacheLocally || this.entityCache.length === 0) { 39 | // Rebuild cache for this entity 40 | const tableClient = TableClient.fromConnectionString(this.storageAccountConnectionString, tableName); 41 | const entities = tableClient.listEntities(); 42 | this.entityCache = []; 43 | for await (const entity of entities) { 44 | // Remove any duplicates which sometimes occur after a watch restart 45 | if (this.entityCache.find((e) => e.rowKey === entity.rowKey) === undefined) { 46 | const e = this.expandPropertyValues(entity as DbEntityType); 47 | this.entityCache.push(e as DbEntity); 48 | } 49 | } 50 | } 51 | return this.entityCache; 52 | } 53 | 54 | async createEntity(tableName: string, rowKey: string, newEntity: DbEntityType): Promise { 55 | 56 | this.entityCache = []; 57 | const entity = this.compressPropertyValues(newEntity) as DbEntityType; 58 | const tableClient = TableClient.fromConnectionString(this.storageAccountConnectionString, tableName); 59 | try { 60 | await tableClient.createEntity({ 61 | partitionKey: tableName, 62 | rowKey, 63 | ...entity 64 | }); 65 | } catch (ex) { 66 | if (ex.response?.status !== 409) { 67 | throw new HttpError(500, ex.message); 68 | } 69 | } 70 | } 71 | 72 | async updateEntity(tableName: string, updatedEntity: DbEntityType): Promise { 73 | 74 | this.entityCache = []; 75 | const e = this.compressPropertyValues(updatedEntity) as DbEntityType; 76 | const tableClient = TableClient.fromConnectionString(this.storageAccountConnectionString, tableName); 77 | await tableClient.updateEntity(e as TableEntity, "Replace"); 78 | 79 | } 80 | 81 | private expandPropertyValues(entity: DbEntityType): DbEntityType { 82 | const result = {} as DbEntityType; 83 | for (const key in entity) { 84 | result[key] = this.expandPropertyValue(entity[key]); 85 | } 86 | return result; 87 | } 88 | 89 | private expandPropertyValue(v: any): any { 90 | if (typeof v === "string" && (v.charAt(0) === '{' || v.charAt(0) === '[')) { 91 | try { 92 | return JSON.parse(v); 93 | } 94 | catch (e) { 95 | return v; 96 | } 97 | } else { 98 | return v; 99 | } 100 | }; 101 | 102 | private compressPropertyValues(entity: DbEntityType): DbEntityType { 103 | const result = {} as DbEntityType; 104 | for (const key in entity) { 105 | result[key] = this.compressPropertyValue(entity[key]); 106 | } 107 | return result; 108 | } 109 | 110 | private compressPropertyValue(v: any): any { 111 | if (typeof v === "object") { 112 | return JSON.stringify(v); 113 | } else { 114 | return v; 115 | } 116 | }; 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/declarative-agent/src/services/IdentityService.ts: -------------------------------------------------------------------------------- 1 | import { HttpRequest } from "@azure/functions"; 2 | import { HttpError } from './Utilities'; 3 | import { Consultant } from '../model/baseModel'; 4 | import { ApiConsultant } from '../model/apiModel'; 5 | 6 | // This is a DEMO ONLY identity solution. 7 | import ConsultantApiService from "./ConsultantApiService"; 8 | 9 | class Identity { 10 | private requestNumber = 1; // Number the requests for logging purposes 11 | 12 | 13 | public async validateRequest(req: HttpRequest): Promise { 14 | 15 | // Default user used for unauthenticated testing 16 | let userId = "1"; 17 | let userName = "Avery Howard"; 18 | let userEmail = "avery@treyresearch.com"; 19 | 20 | // ** INSERT REQUEST VALIDATION HERE (see Lab E6) ** 21 | 22 | // Get the consultant record for this user; create one if necessary 23 | let consultant: ApiConsultant = null; 24 | try { 25 | consultant = await ConsultantApiService.getApiConsultantById(userId); 26 | } 27 | catch (ex) { 28 | if (ex.status !== 404) { 29 | throw ex; 30 | } 31 | // Consultant was not found, so we'll create one below 32 | consultant = null; 33 | } 34 | if (!consultant) consultant = await this.createConsultantForUser(userId, userName, userEmail); 35 | 36 | return consultant; 37 | } 38 | 39 | private async createConsultantForUser(userId: string, userName: string, 40 | userEmail: string): Promise { 41 | 42 | // Create a new consultant record for this user with default values 43 | const consultant: Consultant = { 44 | id: userId, 45 | name: userName, 46 | email: userEmail, 47 | phone: "1-555-123-4567", 48 | consultantPhotoUrl: "https://microsoft.github.io/copilot-camp/demo-assets/images/consultants/Unknown.jpg", 49 | location: { 50 | street: "One Memorial Drive", 51 | city: "Cambridge", 52 | state: "MA", 53 | country: "USA", 54 | postalCode: "02142", 55 | latitude: 42.361366, 56 | longitude: -71.081257 57 | }, 58 | skills: ["JavaScript", "TypeScript"], 59 | certifications: ["Azure Development"], 60 | roles: ["Architect", "Project Lead"] 61 | }; 62 | const result = await ConsultantApiService.createApiConsultant(consultant); 63 | return result; 64 | } 65 | } 66 | 67 | export default new Identity(); 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/declarative-agent/src/services/ProjectApiService.ts: -------------------------------------------------------------------------------- 1 | import { Project, HoursEntry, Assignment } from '../model/baseModel'; 2 | import { ApiProject, ApiAddConsultantToProjectResponse } from '../model/apiModel'; 3 | import ProjectDbService from './ProjectDbService'; 4 | import AssignmentDbService from './AssignmentDbService'; 5 | import ConsultantDbService from './ConsultantDbService'; 6 | import ConsultantApiService from './ConsultantApiService'; 7 | import { HttpError } from './Utilities'; 8 | 9 | class ProjectApiService { 10 | 11 | async getApiProjectById(projectId: string): Promise { 12 | const project = await ProjectDbService.getProjectById(projectId); 13 | let assignments = await AssignmentDbService.getAssignments(); 14 | 15 | const result = await this.getApiProject(project, assignments); 16 | return result; 17 | } 18 | 19 | async getApiProjects(projectOrClientName: string, consultantName: string): Promise { 20 | 21 | let projects = await ProjectDbService.getProjects(); 22 | let assignments = await AssignmentDbService.getAssignments(); 23 | 24 | // Filter on base properties 25 | if (projectOrClientName) { 26 | projects = projects.filter( 27 | (p) => { 28 | const name = p.name?.toLowerCase(); 29 | const clientName = p.clientName?.toLowerCase(); 30 | return name.includes(projectOrClientName.toLowerCase()) || clientName.includes(projectOrClientName.toLowerCase()); 31 | }); 32 | } 33 | //remove duplicates 34 | projects = projects.filter( 35 | (project, index, self) => 36 | index === self.findIndex((p) => ( 37 | p.id === project.id 38 | )) 39 | ); 40 | 41 | // Augment the base properties with assignment information 42 | let result = await Promise.all(projects.map((p) => this.getApiProject(p, assignments))); 43 | 44 | // Filter on augmented properties 45 | if (result && consultantName) { 46 | result = result.filter( 47 | (p) => { 48 | const name = consultantName.toLowerCase(); 49 | return p.consultants.find((n) => n.consultantName.toLowerCase().includes(name)); 50 | }); 51 | }; 52 | 53 | return result; 54 | } 55 | 56 | // Augment a project to get an ApiProject 57 | async getApiProject(project: Project, assignments: Assignment[]): Promise { 58 | 59 | const result = project as ApiProject; 60 | assignments = assignments.filter((a) => a.projectId === project.id); 61 | 62 | result.consultants = []; 63 | result.forecastThisMonth = 0; 64 | result.forecastNextMonth = 0; 65 | result.deliveredLastMonth = 0; 66 | result.deliveredThisMonth = 0; 67 | 68 | for (let assignment of assignments) { 69 | const consultant = await ConsultantDbService.getConsultantById(assignment.consultantId); 70 | const { lastMonthHours: forecastLastMonth, 71 | thisMonthHours: forecastThisMonth, 72 | nextMonthHours: forecastNextMonth } = this.findHours(assignment.forecast); 73 | const { lastMonthHours: deliveredLastMonth, 74 | thisMonthHours: deliveredThisMonth, 75 | nextMonthHours: deliveredNextMonth } = this.findHours(assignment.delivered); 76 | 77 | result.consultants.push({ 78 | consultantName: consultant.name, 79 | consultantLocation: consultant.location, 80 | role: assignment.role, 81 | forecastThisMonth: forecastThisMonth, 82 | forecastNextMonth: forecastNextMonth, 83 | deliveredLastMonth: deliveredLastMonth, 84 | deliveredThisMonth: deliveredThisMonth 85 | }); 86 | 87 | result.forecastThisMonth += forecastThisMonth; 88 | result.forecastNextMonth += forecastNextMonth; 89 | result.deliveredLastMonth += deliveredLastMonth; 90 | result.deliveredThisMonth += deliveredThisMonth; 91 | 92 | } 93 | return result; 94 | } 95 | 96 | // Extract this and next month's hours from an array of HoursEntry 97 | private findHours(hours: HoursEntry[]): { lastMonthHours: number, thisMonthHours: number, nextMonthHours: number } { 98 | const now = new Date(); 99 | const thisMonth = now.getMonth(); 100 | const thisYear = now.getFullYear(); 101 | 102 | const lastMonth = thisMonth === 0 ? 11 : thisMonth - 1; 103 | const lastYear = thisMonth === 0 ? thisYear - 1 : thisYear; 104 | 105 | const nextMonth = thisMonth === 11 ? 0 : thisMonth + 1; 106 | const nextYear = thisMonth === 11 ? thisYear + 1 : thisYear; 107 | 108 | const result = { 109 | lastMonthHours: hours.find((h) => h.month === lastMonth + 1 && h.year === lastYear)?.hours || 0, 110 | thisMonthHours: hours.find((h) => h.month === thisMonth + 1 && h.year === thisYear)?.hours || 0, 111 | nextMonthHours: hours.find((h) => h.month === nextMonth + 1 && h.year === nextYear)?.hours || 0 112 | }; 113 | return result; 114 | } 115 | 116 | async addConsultantToProject(projectName: string, consultantName: string, role: string, hours: number): Promise { 117 | let projects = await this.getApiProjects(projectName, ""); 118 | let consultants = await ConsultantApiService.getApiConsultants(consultantName, "", "", "", "", ""); 119 | 120 | if (projects.length === 0) { 121 | throw new HttpError(404, `Project not found: ${projectName}`); 122 | } else if (projects.length > 1) { 123 | throw new HttpError(406, `Multiple projects found with the name: ${projectName}`); 124 | } else if (consultants.length === 0) { 125 | throw new HttpError(404, `Consultant not found: ${consultantName}`); 126 | } else if (consultants.length > 1) { 127 | throw new HttpError(406, `Multiple consultants found with the name: ${consultantName}`); 128 | } 129 | const project = projects[0]; 130 | const consultant = consultants[0]; 131 | 132 | // Always charge to the current month 133 | const remainingForecast = await AssignmentDbService.addConsultantToProject(project.id, consultant.id, role, hours); 134 | const message = `Added consultant ${consultant.name} to ${project.clientName} on project "${project.name}" with ${remainingForecast} hours forecast this month.`; 135 | 136 | return { 137 | clientName: project.clientName, 138 | projectName: project.name, 139 | consultantName: consultants[0].name, 140 | remainingForecast, 141 | message 142 | } 143 | } 144 | } 145 | 146 | export default new ProjectApiService(); 147 | -------------------------------------------------------------------------------- /src/declarative-agent/src/services/ProjectDbService.ts: -------------------------------------------------------------------------------- 1 | import DbService from './DbService'; 2 | import { DbProject } from '../model/dbModel'; 3 | import { Project } from '../model/baseModel'; 4 | 5 | const TABLE_NAME = "Project"; 6 | 7 | class ProjectDbService { 8 | 9 | // NOTE: Projects are READ ONLY in this demo app, so we are free to cache them in memory. 10 | private dbService = new DbService(true); 11 | 12 | async getProjectById(id: string): Promise { 13 | const project = await this.dbService.getEntityByRowKey(TABLE_NAME, id) as DbProject; 14 | return this.convertDbProject(project); 15 | } 16 | 17 | async getProjects(): Promise { 18 | const projects = await this.dbService.getEntities(TABLE_NAME) as DbProject[]; 19 | return projects.map((p) => this.convertDbProject(p)); 20 | } 21 | 22 | private convertDbProject(dbProject: DbProject): Project { 23 | const result = { 24 | id: dbProject.id, 25 | name: dbProject.name, 26 | description: dbProject.description, 27 | clientName: dbProject.clientName, 28 | clientContact: dbProject.clientContact, 29 | clientEmail: dbProject.clientEmail, 30 | location: dbProject.location, 31 | mapUrl: this.getMapUrl(dbProject) 32 | }; 33 | return result; 34 | } 35 | 36 | private getMapUrl(project: Project): string { 37 | let companyNameKabobCase = project.clientName.toLowerCase().replace(/ /g, "-"); 38 | return `https://microsoft.github.io/copilot-camp/demo-assets/images/maps/${companyNameKabobCase}.jpg`; 39 | } 40 | } 41 | 42 | export default new ProjectDbService(); 43 | -------------------------------------------------------------------------------- /src/declarative-agent/src/services/Utilities.ts: -------------------------------------------------------------------------------- 1 | // Throw this object to return an HTTP error 2 | export class HttpError extends Error { 3 | status: number; 4 | constructor(status: number, message: string) { 5 | super(message); 6 | this.status = status; 7 | } 8 | } 9 | 10 | // Clean up common issues with Copilot parameters 11 | export function cleanUpParameter(name: string, value: string): string { 12 | 13 | let val = value.toLowerCase(); 14 | if (val.toLowerCase().includes("trey") || val.toLowerCase().includes("research")) { 15 | const newVal = val.replace("trey", "").replace("research", "").trim(); 16 | console.log(` ❗ Plugin name detected in the ${name} parameter '${val}'; replacing with '${newVal}'.`); 17 | val = newVal; 18 | } 19 | if (val === "") { 20 | console.log(` ❗ Invalid name '${val}'; replacing with 'avery'.`); 21 | val = "avery"; 22 | } 23 | if (name==="role" && val === "consultant") { 24 | console.log(` ❗ Invalid role name '${val}'; replacing with ''.`); 25 | val = ""; 26 | } 27 | if (val === "null") { 28 | console.log(` ❗ Invalid value '${val}'; replacing with ''.`); 29 | val = ""; 30 | } 31 | return val; 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/declarative-agent/teamsapp.local.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/yaml.schema.json 2 | # Visit https://aka.ms/teamsfx-v5.0-guide for details on this file 3 | # Visit https://aka.ms/teamsfx-actions for details on actions 4 | version: v1.5 5 | 6 | provision: 7 | # Creates a Teams app 8 | - uses: teamsApp/create 9 | with: 10 | # Teams app name 11 | name: TreyResearch${{APP_NAME_SUFFIX}} 12 | # Write the information of created resources into environment file for 13 | # the specified environment variable(s). 14 | writeToEnvironmentFile: 15 | teamsAppId: TEAMS_APP_ID 16 | 17 | # Set required variables for local launch 18 | - uses: script 19 | with: 20 | run: 21 | echo "::set-teamsfx-env FUNC_NAME=me"; 22 | echo "::set-teamsfx-env FUNC_ENDPOINT=http://localhost:7071"; 23 | 24 | # Build Teams app package with latest env value 25 | - uses: teamsApp/zipAppPackage 26 | with: 27 | # Path to manifest template 28 | manifestPath: ./appPackage/manifest.json 29 | outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip 30 | outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json 31 | 32 | # Apply the Teams app manifest to an existing Teams app in 33 | # Teams Developer Portal. 34 | # Will use the app id in manifest file to determine which Teams app to update. 35 | - uses: teamsApp/update 36 | with: 37 | # Relative path to this file. This is the path for built zip file. 38 | appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip 39 | 40 | # Extend your Teams app to Outlook and the Microsoft 365 app 41 | - uses: teamsApp/extendToM365 42 | with: 43 | # Relative path to the build app package. 44 | appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip 45 | # Write the information of created resources into environment file for 46 | # the specified environment variable(s). 47 | writeToEnvironmentFile: 48 | titleId: M365_TITLE_ID 49 | appId: M365_APP_ID 50 | 51 | deploy: 52 | # Install development tool(s) 53 | - uses: devTool/install 54 | with: 55 | func: 56 | version: ~4.0.5530 57 | symlinkDir: ./devTools/func 58 | # Write the information of installed development tool(s) into environment 59 | # file for the specified environment variable(s). 60 | writeToEnvironmentFile: 61 | funcPath: FUNC_PATH 62 | # Generate runtime environment variables 63 | - uses: file/createOrUpdateEnvironmentFile 64 | with: 65 | target: ./.localConfigs 66 | envs: 67 | STORAGE_ACCOUNT_CONNECTION_STRING: ${{SECRET_STORAGE_ACCOUNT_CONNECTION_STRING}} 68 | 69 | # Run npm command 70 | - uses: cli/runNpmCommand 71 | name: install dependencies 72 | with: 73 | args: install --no-audit 74 | 75 | - uses: script 76 | name: Ensure database 77 | with: 78 | run: node db-setup.js 79 | workingDirectory: scripts 80 | -------------------------------------------------------------------------------- /src/declarative-agent/teamsapp.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/yaml.schema.json 2 | # Visit https://aka.ms/teamsfx-v5.0-guide for details on this file 3 | # Visit https://aka.ms/teamsfx-actions for details on actions 4 | version: v1.5 5 | 6 | environmentFolderPath: ./env 7 | 8 | # Triggered when 'teamsapp provision' is executed 9 | provision: 10 | # Creates a Teams app 11 | - uses: teamsApp/create 12 | with: 13 | # Teams app name 14 | name: TreyResearch${{APP_NAME_SUFFIX}} 15 | # Write the information of created resources into environment file for 16 | # the specified environment variable(s). 17 | writeToEnvironmentFile: 18 | teamsAppId: TEAMS_APP_ID 19 | 20 | - uses: arm/deploy # Deploy given ARM templates parallelly. 21 | with: 22 | # AZURE_SUBSCRIPTION_ID is a built-in environment variable, 23 | # if its value is empty, TeamsFx will prompt you to select a subscription. 24 | # Referencing other environment variables with empty values 25 | # will skip the subscription selection prompt. 26 | subscriptionId: ${{AZURE_SUBSCRIPTION_ID}} 27 | # AZURE_RESOURCE_GROUP_NAME is a built-in environment variable, 28 | # if its value is empty, TeamsFx will prompt you to select or create one 29 | # resource group. 30 | # Referencing other environment variables with empty values 31 | # will skip the resource group selection prompt. 32 | resourceGroupName: ${{AZURE_RESOURCE_GROUP_NAME}} 33 | templates: 34 | - path: ./infra/azure.bicep # Relative path to this file 35 | # Relative path to this yaml file. 36 | # Placeholders will be replaced with corresponding environment 37 | # variable before ARM deployment. 38 | parameters: ./infra/azure.parameters.json 39 | # Required when deploying ARM template 40 | deploymentName: Create-resources-for-sme 41 | # Teams Toolkit will download this bicep CLI version from github for you, 42 | # will use bicep CLI in PATH if you remove this config. 43 | bicepCliVersion: v0.9.1 44 | 45 | - uses: script 46 | name: Ensure database 47 | with: 48 | run: node db-setup.js ${{SECRET_STORAGE_ACCOUNT_CONNECTION_STRING}} 49 | workingDirectory: scripts 50 | 51 | # Validate using manifest schema 52 | # - uses: teamsApp/validateManifest 53 | # with: 54 | # # Path to manifest template 55 | # manifestPath: ./appPackage/manifest.json 56 | 57 | # Build Teams app package with latest env value 58 | - uses: teamsApp/zipAppPackage 59 | with: 60 | # Path to manifest template 61 | manifestPath: ./appPackage/manifest.json 62 | outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip 63 | outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json 64 | 65 | # Apply the Teams app manifest to an existing Teams app in 66 | # Teams Developer Portal. 67 | # Will use the app id in manifest file to determine which Teams app to update. 68 | - uses: teamsApp/update 69 | with: 70 | # Relative path to this file. This is the path for built zip file. 71 | appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip 72 | 73 | # Extend your Teams app to Outlook and the Microsoft 365 app 74 | - uses: teamsApp/extendToM365 75 | with: 76 | # Relative path to the build app package. 77 | appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip 78 | # Write the information of created resources into environment file for 79 | # the specified environment variable(s). 80 | writeToEnvironmentFile: 81 | titleId: M365_TITLE_ID 82 | appId: M365_APP_ID 83 | 84 | # Triggered when 'teamsapp deploy' is executed 85 | deploy: 86 | # Run npm command 87 | - uses: cli/runNpmCommand 88 | name: install dependencies 89 | with: 90 | args: install 91 | 92 | - uses: cli/runNpmCommand 93 | name: build app 94 | with: 95 | args: run build --if-present 96 | 97 | # Deploy your application to Azure Functions using the zip deploy feature. 98 | # For additional details, see at https://aka.ms/zip-deploy-to-azure-functions 99 | - uses: azureFunctions/zipDeploy 100 | with: 101 | # deploy base folder 102 | artifactFolder: . 103 | # Ignore file location, leave blank will ignore nothing 104 | ignoreFile: .funcignore 105 | # The resource id of the cloud resource to be deployed to. 106 | # This key will be generated by arm/deploy action automatically. 107 | # You can replace it with your existing Azure Resource id 108 | # or add it to your environment variable file. 109 | resourceId: ${{API_FUNCTION_RESOURCE_ID}} 110 | 111 | # Triggered when 'teamsapp publish' is executed 112 | publish: 113 | # Build Teams app package with latest env value 114 | - uses: teamsApp/zipAppPackage 115 | with: 116 | # Path to manifest template 117 | manifestPath: ./appPackage/manifest.json 118 | outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip 119 | outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json 120 | # Apply the Teams app manifest to an existing Teams app in 121 | # Teams Developer Portal. 122 | # Will use the app id in manifest file to determine which Teams app to update. 123 | - uses: teamsApp/update 124 | with: 125 | # Relative path to this file. This is the path for built zip file. 126 | appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip 127 | # Publish the app to 128 | # Teams Admin Center (https://admin.teams.microsoft.com/policies/manage-apps) 129 | # for review and approval 130 | - uses: teamsApp/publishAppPackage 131 | with: 132 | appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip 133 | # Write the information of created resources into environment file for 134 | # the specified environment variable(s). 135 | writeToEnvironmentFile: 136 | publishedAppId: TEAMS_APP_PUBLISHED_APP_ID 137 | projectId: 4aa98513-2d59-4e3e-8d3c-23c2c89adf2d 138 | -------------------------------------------------------------------------------- /src/declarative-agent/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "dist", 6 | "rootDir": ".", 7 | "sourceMap": true, 8 | "strict": false, 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "typeRoots": ["./node_modules/@types"] 12 | } 13 | } --------------------------------------------------------------------------------