├── .devcontainer ├── Dockerfile ├── devcontainer.json └── first-run-notice.txt ├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── azure.yaml ├── infra ├── abbreviations.json ├── app │ ├── ai-Cog-Service-Access.bicep │ ├── ai.bicep │ ├── api.bicep │ ├── eventgrid.bicep │ ├── storage-Access.bicep │ ├── storage-PrivateEndpoint.bicep │ └── vnet.bicep ├── core │ ├── cognitive │ │ └── ai-textanalytics.bicep │ ├── host │ │ ├── appserviceplan.bicep │ │ └── functions-flexconsumption.bicep │ ├── identity │ │ └── userAssignedIdentity.bicep │ ├── monitor │ │ ├── appinsights-access.bicep │ │ ├── applicationinsights.bicep │ │ ├── loganalytics.bicep │ │ └── monitoring.bicep │ └── storage │ │ └── storage-account.bicep ├── main.bicep └── main.parameters.json ├── scripts ├── post-up.ps1 └── post-up.sh └── text_summarize ├── .funcignore ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── function_app.py ├── host.json └── requirements.txt /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/vscode/devcontainers/universal:latest 2 | 3 | # Copy custom first notice message. 4 | COPY first-run-notice.txt /tmp/staging/ 5 | RUN sudo mv -f /tmp/staging/first-run-notice.txt /usr/local/etc/vscode-dev-containers/ \ 6 | && sudo rm -rf /tmp/staging 7 | 8 | # Install PowerShell 7.x 9 | RUN sudo apt-get update \ 10 | && sudo apt-get install -y wget apt-transport-https software-properties-common \ 11 | && wget -q https://packages.microsoft.com/config/ubuntu/$(. /etc/os-release && echo $VERSION_ID)/packages-microsoft-prod.deb \ 12 | && sudo dpkg -i packages-microsoft-prod.deb \ 13 | && sudo apt-get update \ 14 | && sudo apt-get install -y powershell 15 | 16 | # Install Azure Functions Core Tools 17 | RUN curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg \ 18 | && sudo mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg \ 19 | && sudo sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-ubuntu-$(lsb_release -cs)-prod $(lsb_release -cs) main" > /etc/apt/sources.list.d/dotnetdev.list' \ 20 | && sudo apt-get update \ 21 | && sudo apt-get install -y azure-functions-core-tools-4 22 | 23 | # Install Azure Developer CLI 24 | RUN curl -fsSL https://aka.ms/install-azd.sh | bash 25 | 26 | # Install mechanical-markdown for quickstart validations 27 | RUN pip install mechanical-markdown 28 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Functions Quickstarts Codespace", 3 | "dockerFile": "Dockerfile", 4 | "features": { 5 | "azure-cli": "latest" 6 | }, 7 | "customizations": { 8 | "vscode": { 9 | "extensions": [ 10 | "ms-azuretools.vscode-bicep", 11 | "ms-azuretools.vscode-docker", 12 | "ms-azuretools.vscode-azurefunctions", 13 | "GitHub.copilot", 14 | "humao.rest-client" 15 | ] 16 | } 17 | }, 18 | "mounts": [ 19 | // Mount docker-in-docker library volume 20 | "source=codespaces-linux-var-lib-docker,target=/var/lib/docker,type=volume" 21 | ], 22 | // Always run image-defined docker-init.sh to enable docker-in-docker 23 | "overrideCommand": false, 24 | "remoteUser": "codespace", 25 | "runArgs": [ 26 | // Enable ptrace-based debugging for Go in container 27 | "--cap-add=SYS_PTRACE", 28 | "--security-opt", 29 | "seccomp=unconfined", 30 | 31 | // Enable docker-in-docker configuration 32 | "--init", 33 | "--privileged" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /.devcontainer/first-run-notice.txt: -------------------------------------------------------------------------------- 1 | 👋 Welcome to the Functions Codespace! You are on the Functions Quickstarts image. 2 | It includes everything needed to run through our tutorials and quickstart applications. 3 | 4 | 📚 Functions docs can be found at: https://learn.microsoft.com/en-us/azure/azure-functions/ 5 | -------------------------------------------------------------------------------- /.github/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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | > Please provide us with the following information: 5 | > --------------------------------------------------------------- 6 | 7 | ### This issue is for a: (mark with an `x`) 8 | ``` 9 | - [ ] bug report -> please search issues before submitting 10 | - [ ] feature request 11 | - [ ] documentation issue or request 12 | - [ ] regression (a behavior that used to work and stopped in a new release) 13 | ``` 14 | 15 | ### Minimal steps to reproduce 16 | > 17 | 18 | ### Any log messages given by the failure 19 | > 20 | 21 | ### Expected/desired behavior 22 | > 23 | 24 | ### OS and Version? 25 | > Windows 7, 8 or 10. Linux (which distribution). macOS (Yosemite? El Capitan? Sierra?) 26 | 27 | ### Versions 28 | > 29 | 30 | ### Mention any other details that might be useful 31 | 32 | > --------------------------------------------------------------- 33 | > Thanks! We'll be in touch soon. 34 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | 3 | * ... 4 | 5 | ## Does this introduce a breaking change? 6 | 7 | ``` 8 | [ ] Yes 9 | [ ] No 10 | ``` 11 | 12 | ## Pull Request Type 13 | What kind of change does this Pull Request introduce? 14 | 15 | 16 | ``` 17 | [ ] Bugfix 18 | [ ] Feature 19 | [ ] Code style update (formatting, local variables) 20 | [ ] Refactoring (no functional changes, no api changes) 21 | [ ] Documentation content changes 22 | [ ] Other... Please describe: 23 | ``` 24 | 25 | ## How to Test 26 | * Get the code 27 | 28 | ``` 29 | git clone [repo-address] 30 | cd [repo-name] 31 | git checkout [branch-name] 32 | npm install 33 | ``` 34 | 35 | * Test the code 36 | 37 | ``` 38 | ``` 39 | 40 | ## What to Check 41 | Verify that the following are valid 42 | * ... 43 | 44 | ## Other Information 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions", 4 | "ms-python.python" 5 | ] 6 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach to Python Functions", 6 | "type": "debugpy", 7 | "request": "attach", 8 | "connect": { 9 | "host": "localhost", 10 | "port": 9091 11 | }, 12 | "preLaunchTask": "func: host start" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "azureFunctions.deploySubpath": "text_summarize", 3 | "azureFunctions.scmDoBuildDuringDeployment": true, 4 | "azureFunctions.pythonVenv": ".venv", 5 | "azureFunctions.projectLanguage": "Python", 6 | "azureFunctions.projectRuntime": "~4", 7 | "debug.internalConsoleOptions": "neverOpen", 8 | "azureFunctions.projectLanguageModel": 2 9 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "func", 6 | "label": "func: host start", 7 | "command": "host start", 8 | "problemMatcher": "$func-python-watch", 9 | "isBackground": true, 10 | "dependsOn": "pip install (functions)", 11 | "options": { 12 | "cwd": "${workspaceFolder}/text_summarize" 13 | } 14 | }, 15 | { 16 | "label": "pip install (functions)", 17 | "type": "shell", 18 | "osx": { 19 | "command": "${config:azureFunctions.pythonVenv}/bin/python -m pip install -r requirements.txt" 20 | }, 21 | "windows": { 22 | "command": "${config:azureFunctions.pythonVenv}\\Scripts\\python -m pip install -r requirements.txt" 23 | }, 24 | "linux": { 25 | "command": "${config:azureFunctions.pythonVenv}/bin/python -m pip install -r requirements.txt" 26 | }, 27 | "problemMatcher": [], 28 | "options": { 29 | "cwd": "${workspaceFolder}/text_summarize" 30 | } 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [project-title] Changelog 2 | 3 | 4 | # x.y.z (yyyy-mm-dd) 5 | 6 | *Features* 7 | * ... 8 | 9 | *Bug Fixes* 10 | * ... 11 | 12 | *Breaking Changes* 13 | * ... 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to [project-title] 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 5 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 6 | 7 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 8 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 9 | provided by the bot. You will only need to do this once across all repos using our CLA. 10 | 11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 12 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 13 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 14 | 15 | - [Code of Conduct](#coc) 16 | - [Issues and Bugs](#issue) 17 | - [Feature Requests](#feature) 18 | - [Submission Guidelines](#submit) 19 | 20 | ## Code of Conduct 21 | Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 22 | 23 | ## Found an Issue? 24 | If you find a bug in the source code or a mistake in the documentation, you can help us by 25 | [submitting an issue](#submit-issue) to the GitHub Repository. Even better, you can 26 | [submit a Pull Request](#submit-pr) with a fix. 27 | 28 | ## Want a Feature? 29 | You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub 30 | Repository. If you would like to *implement* a new feature, please submit an issue with 31 | a proposal for your work first, to be sure that we can use it. 32 | 33 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). 34 | 35 | ## Submission Guidelines 36 | 37 | ### Submitting an Issue 38 | Before you submit an issue, search the archive, maybe your question was already answered. 39 | 40 | If your issue appears to be a bug, and hasn't been reported, open a new issue. 41 | Help us to maximize the effort we can spend fixing issues and adding new 42 | features, by not reporting duplicate issues. Providing the following information will increase the 43 | chances of your issue being dealt with quickly: 44 | 45 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps 46 | * **Version** - what version is affected (e.g. 0.1.2) 47 | * **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you 48 | * **Browsers and Operating System** - is this a problem with all browsers? 49 | * **Reproduce the Error** - provide a live example or a unambiguous set of steps 50 | * **Related Issues** - has a similar issue been reported before? 51 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 52 | causing the problem (line of code or commit) 53 | 54 | You can file new issues by providing the above information at the corresponding repository's issues link: https://github.com/[organization-name]/[repository-name]/issues/new]. 55 | 56 | ### Submitting a Pull Request (PR) 57 | Before you submit your Pull Request (PR) consider the following guidelines: 58 | 59 | * Search the repository (https://github.com/[organization-name]/[repository-name]/pulls) for an open or closed PR 60 | that relates to your submission. You don't want to duplicate effort. 61 | 62 | * Make your changes in a new git fork: 63 | 64 | * Commit your changes using a descriptive commit message 65 | * Push your fork to GitHub: 66 | * In GitHub, create a pull request 67 | * If we suggest changes then: 68 | * Make the required updates. 69 | * Rebase your fork and force push to your GitHub repository (this will update your Pull Request): 70 | 71 | ```shell 72 | git rebase master -i 73 | git push -f 74 | ``` 75 | 76 | That's it! Thank you for your contribution! 77 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_type: sample 3 | languages: 4 | - azdeveloper 5 | - python 6 | - bicep 7 | products: 8 | - azure 9 | - azure-functions 10 | - ai-services 11 | - azure-cognitive-search 12 | urlFragment: function-python-ai-textsummarize 13 | name: Azure Functions - Text Summarization using AI Cognitive Language Service (Python v2 Function) 14 | description: This sample shows how to take text documents as a input via BlobTrigger, does Text Summarization & Sentiment Score processing using the AI Congnitive Language service, and then outputs to another text document using BlobOutput binding. Deploys to Flex Consumption hosting plan of Azure Functions. 15 | --- 16 | 17 | 18 | # Azure Functions 19 | ## Text Summarization using AI Cognitive Language Service (Python v2 Function) 20 | 21 | This sample shows how to take text documents as a input via BlobTrigger, does Text Summarization & Sentiment Score processing using the AI Congnitive Language service, and then outputs to another text document using BlobOutput binding. Deploys to Flex Consumption hosting plan of Azure Functions. 22 | 23 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/Azure-Samples/function-python-ai-textsummarize) 24 | 25 | ## Run on your local environment 26 | 27 | ### Pre-reqs 28 | 1) [Python 3.8+](https://www.python.org/) required 29 | 2) [Azure Functions Core Tools](https://learn.microsoft.com/en-us/azure/azure-functions/functions-run-local?tabs=v4%2Cmacos%2Ccsharp%2Cportal%2Cbash#install-the-azure-functions-core-tools) 30 | 3) [Azurite](https://github.com/Azure/Azurite) 31 | 32 | The easiest way to install Azurite is using a Docker container or the support built into Visual Studio: 33 | ```bash 34 | docker run -d -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite 35 | ``` 36 | 37 | 4) Once you have your Azure subscription, run the following in a new terminal window to create all the AI Language and other resources needed: 38 | ```bash 39 | azd provision 40 | ``` 41 | 42 | Take note of the value of `TEXT_ANALYTICS_ENDPOINT` which can be found in `./.azure//.env`. It will look something like: 43 | ```bash 44 | TEXT_ANALYTICS_ENDPOINT="https://.cognitiveservices.azure.com/" 45 | ``` 46 | 47 | Alternatively you can [create a Language resource](https://portal.azure.com/#create/Microsoft.CognitiveServicesTextAnalytics) in the Azure portal to get your key and endpoint. After it deploys, click Go to resource and view the Endpoint value. 48 | 49 | 5) [Azure Storage Explorer](https://azure.microsoft.com/en-us/products/storage/storage-explorer/) or storage explorer features of [Azure Portal](https://portal.azure.com) 50 | 6) Add this `local.settings.json` file to the `./text_summarization` folder to simplify local development. Optionally fill in the AI_URL and AI_SECRET values per step 4. This file will be gitignored to protect secrets from committing to your repo. 51 | ```json 52 | { 53 | "IsEncrypted": false, 54 | "Values": { 55 | "AzureWebJobsStorage": "UseDevelopmentStorage=true", 56 | "FUNCTIONS_WORKER_RUNTIME": "python", 57 | "TEXT_ANALYTICS_ENDPOINT": "" 58 | } 59 | } 60 | ``` 61 | 62 | 63 | ### Using Visual Studio 64 | 1) Open `text_summarization.sln` using Visual Studio 2022 or later. 65 | 2) Press Run (`F5`) to run in the debugger 66 | 3) Open Storage Explorer, Storage Accounts -> Emulator -> Blob Containers -> and create a container `unprocessed-text` if it does not already exists 67 | 4) Copy any .txt document file with text into the `unprocessed-text` container 68 | 69 | You will see AI analysis happen in the Terminal standard out. The analysis will be saved in a .txt file in the `processed-text` blob container. 70 | 71 | ### Using VS Code 72 | 1) Open the root folder in VS Code: 73 | 74 | ```bash 75 | code . 76 | ``` 77 | 2) Ensure `local.settings.json` exists already using steps above 78 | 3) Run and Debug by pressing `F5` 79 | 4) Open Storage Explorer, Storage Accounts -> Emulator -> Blob Containers -> and create a container `unprocessed-text` if it does not already exists 80 | 5) Copy any .txt document file with text into the `unprocessed-text` container 81 | 82 | You will see AI analysis happen in the Terminal standard out. The analysis will be saved in a .txt file in the `processed-text` blob container. 83 | 84 | ### Using Functions Core Tools CLI 85 | 0) Ensure `local.settings.json` exists already using steps above 86 | 1) Open a new terminal and do the following: 87 | 88 | ```bash 89 | cd text_summarization 90 | func start 91 | ``` 92 | 2) Open Storage Explorer, Storage Accounts -> Emulator -> Blob Containers -> and create a container `test-samples-trigger` if it does not already exists 93 | 3) Copy any .txt document file with text into the `test-samples-trigger` container 94 | 95 | You will see AI analysis happen in the Terminal standard out. The analysis will be saved in a .txt file in the `test-samples-output` blob container. 96 | 97 | ## Deploy to Azure 98 | 99 | The easiest way to deploy this app is using the [Azure Developer CLI](https://aka.ms/azd). If you open this repo in GitHub CodeSpaces the AZD tooling is already preinstalled. 100 | 101 | To provision and deploy: 102 | 1) Open a new terminal and do the following from root folder: 103 | ```bash 104 | azd up 105 | ``` 106 | 107 | ## Understand the Code 108 | 109 | The main operation of the code starts with the `summarize_function` function in [function_app.py](./text_summarize/function_app.py). The function is triggered by a Blob uploaded event using BlobTrigger with EventGrid, your code runs to do the processing with AI, and then the output is returned as another blob file simply by returning a value and using the BlobOutput binding. 110 | 111 | ```python 112 | @app.function_name(name="summarize_function") 113 | @app.blob_trigger(arg_name="myblob", path="unprocessed-text/{name}", 114 | connection="AzureWebJobsStorage", source="EventGrid") 115 | @app.blob_output(arg_name="outputblob", path="processed-text/{name}-output.txt", connection="AzureWebJobsStorage") 116 | def test_function(myblob: func.InputStream, outputblob: func.Out[str]): 117 | logging.info(f"Triggered item: {myblob.name}\n") 118 | 119 | document = [myblob.read().decode('utf-8')] 120 | summarized_text = ai_summarize_txt(document) 121 | logging.info(f"\n *****Summary***** \n{summarized_text}"); 122 | outputblob.set(summarized_text) 123 | ``` 124 | 125 | The `ai_summarize_txt` helper function does the heavy lifting for summary extraction and sentiment analysis using the `TextAnalyticsClient` SDK from the [AI Language Services](https://learn.microsoft.com/en-us/azure/ai-services/language-service/): 126 | 127 | ```python 128 | def ai_summarize_txt(document): 129 | 130 | poller = text_analytics_client.begin_extract_summary(document) 131 | extract_summary_results = poller.result() 132 | 133 | summarized_text = "" 134 | document_results = poller.result() 135 | for result in extract_summary_results: 136 | if result.kind == "ExtractiveSummarization": 137 | summarized_text= "Summary extracted: \n{}".format( 138 | " ".join([sentence.text for sentence in result.sentences])) 139 | print(summarized_text) 140 | logging.info(f"Returning summarized text: \n{summarized_text}") 141 | elif result.is_error is True: 142 | print("...Is an error with code '{}' and message '{}'".format( 143 | result.error.code, result.error.message 144 | )) 145 | logging.error(f"Error with code '{result.error.code}' and message '{result.error.message}'") 146 | 147 | # Perform sentiment analysis on document summary 148 | sentiment_result = text_analytics_client.analyze_sentiment([summarized_text])[0] 149 | print(f"\nSentiment: {sentiment_result.sentiment}") 150 | print(f"Positive Score: {sentiment_result.confidence_scores.positive}") 151 | print(f"Negative Score: {sentiment_result.confidence_scores.negative}") 152 | print(f"Neutral Score: {sentiment_result.confidence_scores.neutral}") 153 | 154 | summary_with_sentiment = summarized_text + f"\nSentiment: {sentiment_result.sentiment}\n" 155 | 156 | return summary_with_sentiment 157 | ``` 158 | -------------------------------------------------------------------------------- /azure.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json 2 | 3 | name: text-summzarize-python-ai-func 4 | metadata: 5 | template: text-summzarize-python-ai-func@1.0.0 6 | hooks: 7 | postdeploy: 8 | windows: 9 | shell: pwsh 10 | run: ./scripts/post-up.ps1 11 | interactive: true 12 | continueOnError: false 13 | posix: 14 | shell: sh 15 | run: ./scripts/post-up.sh 16 | interactive: true 17 | continueOnError: false 18 | services: 19 | api: 20 | project: ./text_summarize/ 21 | language: python 22 | host: function 23 | -------------------------------------------------------------------------------- /infra/abbreviations.json: -------------------------------------------------------------------------------- 1 | { 2 | "analysisServicesServers": "as", 3 | "apiManagementService": "apim-", 4 | "appConfigurationConfigurationStores": "appcs-", 5 | "appManagedEnvironments": "cae-", 6 | "appContainerApps": "ca-", 7 | "authorizationPolicyDefinitions": "policy-", 8 | "automationAutomationAccounts": "aa-", 9 | "blueprintBlueprints": "bp-", 10 | "blueprintBlueprintsArtifacts": "bpa-", 11 | "cacheRedis": "redis-", 12 | "cdnProfiles": "cdnp-", 13 | "cdnProfilesEndpoints": "cdne-", 14 | "cognitiveServicesAccounts": "cog-", 15 | "cognitiveServicesFormRecognizer": "cog-fr-", 16 | "cognitiveServicesTextAnalytics": "cog-ta-", 17 | "computeAvailabilitySets": "avail-", 18 | "computeCloudServices": "cld-", 19 | "computeDiskEncryptionSets": "des", 20 | "computeDisks": "disk", 21 | "computeDisksOs": "osdisk", 22 | "computeGalleries": "gal", 23 | "computeSnapshots": "snap-", 24 | "computeVirtualMachines": "vm", 25 | "computeVirtualMachineScaleSets": "vmss-", 26 | "containerInstanceContainerGroups": "ci", 27 | "containerRegistryRegistries": "cr", 28 | "containerServiceManagedClusters": "aks-", 29 | "databricksWorkspaces": "dbw-", 30 | "dataFactoryFactories": "adf-", 31 | "dataLakeAnalyticsAccounts": "dla", 32 | "dataLakeStoreAccounts": "dls", 33 | "dataMigrationServices": "dms-", 34 | "dBforMySQLServers": "mysql-", 35 | "dBforPostgreSQLServers": "psql-", 36 | "devicesIotHubs": "iot-", 37 | "devicesProvisioningServices": "provs-", 38 | "devicesProvisioningServicesCertificates": "pcert-", 39 | "documentDBDatabaseAccounts": "cosmos-", 40 | "eventGridDomains": "evgd-", 41 | "eventGridDomainsTopics": "evgt-", 42 | "eventGridEventSubscriptions": "evgs-", 43 | "eventHubNamespaces": "evhns-", 44 | "eventHubNamespacesEventHubs": "evh-", 45 | "hdInsightClustersHadoop": "hadoop-", 46 | "hdInsightClustersHbase": "hbase-", 47 | "hdInsightClustersKafka": "kafka-", 48 | "hdInsightClustersMl": "mls-", 49 | "hdInsightClustersSpark": "spark-", 50 | "hdInsightClustersStorm": "storm-", 51 | "hybridComputeMachines": "arcs-", 52 | "insightsActionGroups": "ag-", 53 | "insightsComponents": "appi-", 54 | "keyVaultVaults": "kv-", 55 | "kubernetesConnectedClusters": "arck", 56 | "kustoClusters": "dec", 57 | "kustoClustersDatabases": "dedb", 58 | "logicIntegrationAccounts": "ia-", 59 | "logicWorkflows": "logic-", 60 | "machineLearningServicesWorkspaces": "mlw-", 61 | "managedIdentityUserAssignedIdentities": "id-", 62 | "managementManagementGroups": "mg-", 63 | "migrateAssessmentProjects": "migr-", 64 | "networkApplicationGateways": "agw-", 65 | "networkApplicationSecurityGroups": "asg-", 66 | "networkAzureFirewalls": "afw-", 67 | "networkBastionHosts": "bas-", 68 | "networkConnections": "con-", 69 | "networkDnsZones": "dnsz-", 70 | "networkExpressRouteCircuits": "erc-", 71 | "networkFirewallPolicies": "afwp-", 72 | "networkFirewallPoliciesWebApplication": "waf", 73 | "networkFirewallPoliciesRuleGroups": "wafrg", 74 | "networkFrontDoors": "fd-", 75 | "networkFrontdoorWebApplicationFirewallPolicies": "fdfp-", 76 | "networkLoadBalancersExternal": "lbe-", 77 | "networkLoadBalancersInternal": "lbi-", 78 | "networkLoadBalancersInboundNatRules": "rule-", 79 | "networkLocalNetworkGateways": "lgw-", 80 | "networkNatGateways": "ng-", 81 | "networkNetworkInterfaces": "nic-", 82 | "networkNetworkSecurityGroups": "nsg-", 83 | "networkNetworkSecurityGroupsSecurityRules": "nsgsr-", 84 | "networkNetworkWatchers": "nw-", 85 | "networkPrivateDnsZones": "pdnsz-", 86 | "networkPrivateLinkServices": "pl-", 87 | "networkPublicIPAddresses": "pip-", 88 | "networkPublicIPPrefixes": "ippre-", 89 | "networkRouteFilters": "rf-", 90 | "networkRouteTables": "rt-", 91 | "networkRouteTablesRoutes": "udr-", 92 | "networkTrafficManagerProfiles": "traf-", 93 | "networkVirtualNetworkGateways": "vgw-", 94 | "networkVirtualNetworks": "vnet-", 95 | "networkVirtualNetworksSubnets": "snet-", 96 | "networkVirtualNetworksVirtualNetworkPeerings": "peer-", 97 | "networkVirtualWans": "vwan-", 98 | "networkVpnGateways": "vpng-", 99 | "networkVpnGatewaysVpnConnections": "vcn-", 100 | "networkVpnGatewaysVpnSites": "vst-", 101 | "notificationHubsNamespaces": "ntfns-", 102 | "notificationHubsNamespacesNotificationHubs": "ntf-", 103 | "operationalInsightsWorkspaces": "log-", 104 | "portalDashboards": "dash-", 105 | "powerBIDedicatedCapacities": "pbi-", 106 | "purviewAccounts": "pview-", 107 | "recoveryServicesVaults": "rsv-", 108 | "resourcesResourceGroups": "rg-", 109 | "searchSearchServices": "srch-", 110 | "serviceBusNamespaces": "sb-", 111 | "serviceBusNamespacesQueues": "sbq-", 112 | "serviceBusNamespacesTopics": "sbt-", 113 | "serviceEndPointPolicies": "se-", 114 | "serviceFabricClusters": "sf-", 115 | "signalRServiceSignalR": "sigr", 116 | "sqlManagedInstances": "sqlmi-", 117 | "sqlServers": "sql-", 118 | "sqlServersDataWarehouse": "sqldw-", 119 | "sqlServersDatabases": "sqldb-", 120 | "sqlServersDatabasesStretch": "sqlstrdb-", 121 | "storageStorageAccounts": "st", 122 | "storageStorageAccountsVm": "stvm", 123 | "storSimpleManagers": "ssimp", 124 | "streamAnalyticsCluster": "asa-", 125 | "synapseWorkspaces": "syn", 126 | "synapseWorkspacesAnalyticsWorkspaces": "synw", 127 | "synapseWorkspacesSqlPoolsDedicated": "syndp", 128 | "synapseWorkspacesSqlPoolsSpark": "synsp", 129 | "timeSeriesInsightsEnvironments": "tsi-", 130 | "webServerFarms": "plan-", 131 | "webSitesAppService": "app-", 132 | "webSitesAppServiceEnvironment": "ase-", 133 | "webSitesFunctions": "func-", 134 | "webStaticSites": "stapp-" 135 | } -------------------------------------------------------------------------------- /infra/app/ai-Cog-Service-Access.bicep: -------------------------------------------------------------------------------- 1 | param principalID string 2 | param principalType string = 'ServicePrincipal' // Workaround for https://learn.microsoft.com/en-us/azure/role-based-access-control/role-assignments-template#new-service-principal 3 | param roleDefinitionID string 4 | param aiResourceName string 5 | 6 | resource cognitiveService 'Microsoft.CognitiveServices/accounts@2023-05-01' existing = { 7 | name: aiResourceName 8 | } 9 | 10 | // Allow access from API to this resource using a managed identity and least priv role grants 11 | resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 12 | name: guid(cognitiveService.id, principalID, roleDefinitionID) 13 | scope: cognitiveService 14 | properties: { 15 | roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionID) 16 | principalId: principalID 17 | principalType: principalType 18 | } 19 | } 20 | 21 | output ROLE_ASSIGNMENT_NAME string = roleAssignment.name 22 | -------------------------------------------------------------------------------- /infra/app/ai.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | param customSubDomainName string 5 | 6 | module aiLanguageService '../core/cognitive/ai-textanalytics.bicep' = { 7 | name: 'ai-textanalytics' 8 | params: { 9 | aiResourceName: name 10 | location: location 11 | tags: tags 12 | customSubDomainName: customSubDomainName 13 | } 14 | } 15 | 16 | output name string = aiLanguageService.outputs.name 17 | output url string = aiLanguageService.outputs.url 18 | -------------------------------------------------------------------------------- /infra/app/api.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | param applicationInsightsName string = '' 5 | param appServicePlanId string 6 | param appSettings object = {} 7 | param runtimeName string 8 | param runtimeVersion string 9 | param serviceName string = 'api' 10 | param storageAccountName string 11 | param deploymentStorageContainerName string 12 | param virtualNetworkSubnetId string = '' 13 | param instanceMemoryMB int = 2048 14 | param maximumInstanceCount int = 100 15 | param identityId string = '' 16 | param identityClientId string = '' 17 | param aiLanguageServiceUrl string = '' 18 | 19 | var applicationInsightsIdentity = 'ClientId=${identityClientId};Authorization=AAD' 20 | 21 | module api '../core/host/functions-flexconsumption.bicep' = { 22 | name: '${serviceName}-functions-module' 23 | params: { 24 | name: name 25 | location: location 26 | tags: union(tags, { 'azd-service-name': serviceName }) 27 | identityType: 'UserAssigned' 28 | identityId: identityId 29 | appSettings: union(appSettings, 30 | { 31 | AzureWebJobsStorage__clientId : identityClientId 32 | APPLICATIONINSIGHTS_AUTHENTICATION_STRING: applicationInsightsIdentity 33 | TEXT_ANALYTICS_ENDPOINT: aiLanguageServiceUrl 34 | AZURE_CLIENT_ID: identityClientId 35 | }) 36 | applicationInsightsName: applicationInsightsName 37 | appServicePlanId: appServicePlanId 38 | runtimeName: runtimeName 39 | runtimeVersion: runtimeVersion 40 | storageAccountName: storageAccountName 41 | deploymentStorageContainerName: deploymentStorageContainerName 42 | virtualNetworkSubnetId: virtualNetworkSubnetId 43 | instanceMemoryMB: instanceMemoryMB 44 | maximumInstanceCount: maximumInstanceCount 45 | } 46 | } 47 | 48 | output SERVICE_API_NAME string = api.outputs.name 49 | output SERVICE_API_IDENTITY_PRINCIPAL_ID string = api.outputs.identityPrincipalId 50 | -------------------------------------------------------------------------------- /infra/app/eventgrid.bicep: -------------------------------------------------------------------------------- 1 | param location string = resourceGroup().location 2 | param tags object = {} 3 | param storageAccountId string 4 | 5 | resource unprocessedPdfSystemTopic 'Microsoft.EventGrid/systemTopics@2024-06-01-preview' = { 6 | name: 'unprocessed-pdf-topic' 7 | location: location 8 | tags: tags 9 | properties: { 10 | source: storageAccountId 11 | topicType: 'Microsoft.Storage.StorageAccounts' 12 | } 13 | } 14 | 15 | // The actual event grid subscription will be created in the post deployment script as it needs the function to be deployed first 16 | 17 | // resource unprocessedPdfSystemTopicSubscription 'Microsoft.EventGrid/systemTopics/eventSubscriptions@2024-06-01-preview' = { 18 | // parent: unprocessedPdfSystemTopic 19 | // name: 'unprocessed-pdf-topic-subscription' 20 | // properties: { 21 | // destination: { 22 | // endpointType: 'WebHook' 23 | // properties: { 24 | // //Will be set on post-deployment script once the function is created and the blobs extension code is available 25 | // //endpointUrl: 'https://${function_app_blob_event_grid_name}.azurewebsites.net/runtime/webhooks/blobs?functionName=Host.Functions.Trigger_BlobEventGrid&code=${blobs_extension}' 26 | // } 27 | // } 28 | // filter: { 29 | // includedEventTypes: [ 30 | // 'Microsoft.Storage.BlobCreated' 31 | // ] 32 | // subjectBeginsWith: '/blobServices/default/containers/${unprocessedPdfContainerName}/' 33 | // } 34 | // } 35 | // } 36 | 37 | output unprocessedPdfSystemTopicId string = unprocessedPdfSystemTopic.id 38 | output unprocessedPdfSystemTopicName string = unprocessedPdfSystemTopic.name 39 | -------------------------------------------------------------------------------- /infra/app/storage-Access.bicep: -------------------------------------------------------------------------------- 1 | param principalID string 2 | param principalType string = 'ServicePrincipal' // Workaround for https://learn.microsoft.com/en-us/azure/role-based-access-control/role-assignments-template#new-service-principal 3 | param roleDefinitionID string 4 | param storageAccountName string 5 | 6 | resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' existing = { 7 | name: storageAccountName 8 | } 9 | 10 | // Allow access from API to storage account using a managed identity and least priv Storage roles 11 | resource storageRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 12 | name: guid(storageAccount.id, principalID, roleDefinitionID) 13 | scope: storageAccount 14 | properties: { 15 | roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionID) 16 | principalId: principalID 17 | principalType: principalType 18 | } 19 | } 20 | 21 | output ROLE_ASSIGNMENT_NAME string = storageRoleAssignment.name 22 | -------------------------------------------------------------------------------- /infra/app/storage-PrivateEndpoint.bicep: -------------------------------------------------------------------------------- 1 | // Parameters 2 | @description('Specifies the name of the virtual network.') 3 | param virtualNetworkName string 4 | 5 | @description('Specifies the name of the subnet which contains the virtual machine.') 6 | param subnetName string 7 | 8 | @description('Specifies the resource name of the Storage resource with an endpoint.') 9 | param resourceName string 10 | 11 | @description('Specifies the location.') 12 | param location string = resourceGroup().location 13 | 14 | param tags object = {} 15 | 16 | // Virtual Network 17 | resource vnet 'Microsoft.Network/virtualNetworks@2021-08-01' existing = { 18 | name: virtualNetworkName 19 | } 20 | 21 | resource storageAccount 'Microsoft.Storage/storageAccounts@2021-09-01' existing = { 22 | name: resourceName 23 | } 24 | 25 | var blobPrivateDNSZoneName = format('privatelink.blob.{0}', environment().suffixes.storage) 26 | var blobPrivateDnsZoneVirtualNetworkLinkName = format('{0}-link-{1}', resourceName, take(toLower(uniqueString(resourceName, virtualNetworkName)), 4)) 27 | 28 | // Private DNS Zones 29 | resource blobPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = { 30 | name: blobPrivateDNSZoneName 31 | location: 'global' 32 | tags: tags 33 | properties: {} 34 | dependsOn: [ 35 | vnet 36 | ] 37 | } 38 | 39 | // Virtual Network Links 40 | resource blobPrivateDnsZoneVirtualNetworkLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = { 41 | parent: blobPrivateDnsZone 42 | name: blobPrivateDnsZoneVirtualNetworkLinkName 43 | location: 'global' 44 | tags: tags 45 | properties: { 46 | registrationEnabled: false 47 | virtualNetwork: { 48 | id: vnet.id 49 | } 50 | } 51 | } 52 | 53 | // Private Endpoints 54 | resource blobPrivateEndpoint 'Microsoft.Network/privateEndpoints@2021-08-01' = { 55 | name: 'blob-private-endpoint' 56 | location: location 57 | tags: tags 58 | properties: { 59 | privateLinkServiceConnections: [ 60 | { 61 | name: 'blobPrivateLinkConnection' 62 | properties: { 63 | privateLinkServiceId: storageAccount.id 64 | groupIds: [ 65 | 'blob' 66 | ] 67 | } 68 | } 69 | ] 70 | subnet: { 71 | id: '${vnet.id}/subnets/${subnetName}' 72 | } 73 | } 74 | } 75 | 76 | resource blobPrivateDnsZoneGroupName 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2022-01-01' = { 77 | parent: blobPrivateEndpoint 78 | name: 'blobPrivateDnsZoneGroup' 79 | properties: { 80 | privateDnsZoneConfigs: [ 81 | { 82 | name: 'storageBlobARecord' 83 | properties: { 84 | privateDnsZoneId: blobPrivateDnsZone.id 85 | } 86 | } 87 | ] 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /infra/app/vnet.bicep: -------------------------------------------------------------------------------- 1 | @description('Specifies the name of the virtual network.') 2 | param vNetName string 3 | 4 | @description('Specifies the location.') 5 | param location string = resourceGroup().location 6 | 7 | @description('Specifies the name of the subnet for the Service Bus private endpoint.') 8 | param peSubnetName string = 'private-endpoints-subnet' 9 | 10 | @description('Specifies the name of the subnet for Function App virtual network integration.') 11 | param appSubnetName string = 'app' 12 | 13 | param tags object = {} 14 | 15 | resource virtualNetwork 'Microsoft.Network/virtualNetworks@2023-05-01' = { 16 | name: vNetName 17 | location: location 18 | tags: tags 19 | properties: { 20 | addressSpace: { 21 | addressPrefixes: [ 22 | '10.0.0.0/16' 23 | ] 24 | } 25 | encryption: { 26 | enabled: false 27 | enforcement: 'AllowUnencrypted' 28 | } 29 | subnets: [ 30 | { 31 | name: peSubnetName 32 | id: resourceId('Microsoft.Network/virtualNetworks/subnets', vNetName, 'private-endpoints-subnet') 33 | properties: { 34 | addressPrefixes: [ 35 | '10.0.1.0/24' 36 | ] 37 | delegations: [] 38 | privateEndpointNetworkPolicies: 'Disabled' 39 | privateLinkServiceNetworkPolicies: 'Enabled' 40 | } 41 | type: 'Microsoft.Network/virtualNetworks/subnets' 42 | } 43 | { 44 | name: appSubnetName 45 | id: resourceId('Microsoft.Network/virtualNetworks/subnets', vNetName, 'app') 46 | properties: { 47 | addressPrefixes: [ 48 | '10.0.2.0/24' 49 | ] 50 | delegations: [ 51 | { 52 | name: 'delegation' 53 | id: resourceId('Microsoft.Network/virtualNetworks/subnets/delegations', vNetName, 'app', 'delegation') 54 | properties: { 55 | //Microsoft.App/environments is the correct delegation for Flex Consumption VNet integration 56 | serviceName: 'Microsoft.App/environments' 57 | } 58 | type: 'Microsoft.Network/virtualNetworks/subnets/delegations' 59 | } 60 | ] 61 | privateEndpointNetworkPolicies: 'Disabled' 62 | privateLinkServiceNetworkPolicies: 'Enabled' 63 | } 64 | type: 'Microsoft.Network/virtualNetworks/subnets' 65 | } 66 | ] 67 | virtualNetworkPeerings: [] 68 | enableDdosProtection: false 69 | } 70 | } 71 | 72 | output peSubnetName string = virtualNetwork.properties.subnets[0].name 73 | output peSubnetID string = virtualNetwork.properties.subnets[0].id 74 | output appSubnetName string = virtualNetwork.properties.subnets[1].name 75 | output appSubnetID string = virtualNetwork.properties.subnets[1].id 76 | -------------------------------------------------------------------------------- /infra/core/cognitive/ai-textanalytics.bicep: -------------------------------------------------------------------------------- 1 | param aiResourceName string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | param sku string = 'S' 5 | param customSubDomainName string 6 | 7 | param principalIds array = [] 8 | 9 | resource cognitiveService 'Microsoft.CognitiveServices/accounts@2023-05-01' = { 10 | name: aiResourceName 11 | sku: { 12 | name: sku 13 | } 14 | tags: tags 15 | location: location 16 | kind: 'TextAnalytics' 17 | properties: { 18 | customSubDomainName: customSubDomainName 19 | } 20 | } 21 | 22 | output name string = cognitiveService.name 23 | output url string = cognitiveService.properties.endpoint 24 | -------------------------------------------------------------------------------- /infra/core/host/appserviceplan.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | param kind string = '' 6 | param reserved bool = true 7 | param sku object 8 | 9 | resource appServicePlan 'Microsoft.Web/serverfarms@2023-12-01' = { 10 | name: name 11 | location: location 12 | tags: tags 13 | sku: sku 14 | kind: kind 15 | properties: { 16 | reserved: reserved 17 | } 18 | } 19 | 20 | output id string = appServicePlan.id 21 | -------------------------------------------------------------------------------- /infra/core/host/functions-flexconsumption.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | // Reference Properties 6 | param applicationInsightsName string = '' 7 | param appServicePlanId string 8 | param storageAccountName string 9 | param virtualNetworkSubnetId string = '' 10 | @allowed(['SystemAssigned', 'UserAssigned']) 11 | param identityType string 12 | @description('User assigned identity name') 13 | param identityId string 14 | 15 | // Runtime Properties 16 | @allowed([ 17 | 'dotnet-isolated', 'node', 'python', 'java', 'powershell', 'custom' 18 | ]) 19 | param runtimeName string 20 | @allowed(['3.10', '3.11', '7.4', '8.0', '10', '11', '17', '20']) 21 | param runtimeVersion string 22 | param kind string = 'functionapp,linux' 23 | 24 | // Microsoft.Web/sites/config 25 | param appSettings object = {} 26 | param instanceMemoryMB int = 2048 27 | param maximumInstanceCount int = 100 28 | param deploymentStorageContainerName string 29 | 30 | resource stg 'Microsoft.Storage/storageAccounts@2022-09-01' existing = { 31 | name: storageAccountName 32 | } 33 | 34 | resource functions 'Microsoft.Web/sites@2023-12-01' = { 35 | name: name 36 | location: location 37 | tags: tags 38 | kind: kind 39 | identity: { 40 | type: identityType 41 | userAssignedIdentities: { 42 | '${identityId}': {} 43 | } 44 | } 45 | properties: { 46 | serverFarmId: appServicePlanId 47 | functionAppConfig: { 48 | deployment: { 49 | storage: { 50 | type: 'blobContainer' 51 | value: '${stg.properties.primaryEndpoints.blob}${deploymentStorageContainerName}' 52 | authentication: { 53 | type: identityType == 'SystemAssigned' ? 'SystemAssignedIdentity' : 'UserAssignedIdentity' 54 | userAssignedIdentityResourceId: identityType == 'UserAssigned' ? identityId : '' 55 | } 56 | } 57 | } 58 | scaleAndConcurrency: { 59 | instanceMemoryMB: instanceMemoryMB 60 | maximumInstanceCount: maximumInstanceCount 61 | } 62 | runtime: { 63 | name: runtimeName 64 | version: runtimeVersion 65 | } 66 | } 67 | virtualNetworkSubnetId: !empty(virtualNetworkSubnetId) ? virtualNetworkSubnetId : null 68 | } 69 | 70 | resource configAppSettings 'config' = { 71 | name: 'appsettings' 72 | properties: union(appSettings, 73 | { 74 | AzureWebJobsStorage__accountName: stg.name 75 | AzureWebJobsStorage__credential : 'managedidentity' 76 | APPLICATIONINSIGHTS_CONNECTION_STRING: applicationInsights.properties.ConnectionString 77 | }) 78 | } 79 | } 80 | 81 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (!empty(applicationInsightsName)) { 82 | name: applicationInsightsName 83 | } 84 | 85 | output name string = functions.name 86 | output uri string = 'https://${functions.properties.defaultHostName}' 87 | output identityPrincipalId string = identityType == 'SystemAssigned' ? functions.identity.principalId : '' 88 | -------------------------------------------------------------------------------- /infra/core/identity/userAssignedIdentity.bicep: -------------------------------------------------------------------------------- 1 | param identityName string 2 | param location string 3 | param tags object = {} 4 | 5 | resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-07-31-preview' = { 6 | name: identityName 7 | location: location 8 | tags: tags 9 | } 10 | 11 | output identityId string = userAssignedIdentity.id 12 | output identityName string = userAssignedIdentity.name 13 | output identityPrincipalId string = userAssignedIdentity.properties.principalId 14 | output identityClientId string = userAssignedIdentity.properties.clientId 15 | -------------------------------------------------------------------------------- /infra/core/monitor/appinsights-access.bicep: -------------------------------------------------------------------------------- 1 | param principalID string 2 | param roleDefinitionID string 3 | param appInsightsName string 4 | 5 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = { 6 | name: appInsightsName 7 | } 8 | 9 | // Allow access from API to app insights using a managed identity and least priv role 10 | resource appInsightsRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { 11 | name: guid(applicationInsights.id, principalID, roleDefinitionID) 12 | scope: applicationInsights 13 | properties: { 14 | roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionID) 15 | principalId: principalID 16 | principalType: 'ServicePrincipal' // Workaround for https://learn.microsoft.com/en-us/azure/role-based-access-control/role-assignments-template#new-service-principal 17 | } 18 | } 19 | 20 | output ROLE_ASSIGNMENT_NAME string = appInsightsRoleAssignment.name 21 | 22 | -------------------------------------------------------------------------------- /infra/core/monitor/applicationinsights.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | param logAnalyticsWorkspaceId string 6 | param disableLocalAuth bool = false 7 | 8 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { 9 | name: name 10 | location: location 11 | tags: tags 12 | kind: 'web' 13 | properties: { 14 | Application_Type: 'web' 15 | WorkspaceResourceId: logAnalyticsWorkspaceId 16 | DisableLocalAuth: disableLocalAuth 17 | } 18 | } 19 | 20 | output connectionString string = applicationInsights.properties.ConnectionString 21 | output instrumentationKey string = applicationInsights.properties.InstrumentationKey 22 | output name string = applicationInsights.name 23 | -------------------------------------------------------------------------------- /infra/core/monitor/loganalytics.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = { 6 | name: name 7 | location: location 8 | tags: tags 9 | properties: any({ 10 | retentionInDays: 30 11 | features: { 12 | searchVersion: 1 13 | } 14 | sku: { 15 | name: 'PerGB2018' 16 | } 17 | }) 18 | } 19 | 20 | output id string = logAnalytics.id 21 | output name string = logAnalytics.name 22 | -------------------------------------------------------------------------------- /infra/core/monitor/monitoring.bicep: -------------------------------------------------------------------------------- 1 | param logAnalyticsName string 2 | param applicationInsightsName string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | param disableLocalAuth bool = false 6 | 7 | module logAnalytics 'loganalytics.bicep' = { 8 | name: 'loganalytics' 9 | params: { 10 | name: logAnalyticsName 11 | location: location 12 | tags: tags 13 | } 14 | } 15 | 16 | module applicationInsights 'applicationinsights.bicep' = { 17 | name: 'applicationinsights' 18 | params: { 19 | name: applicationInsightsName 20 | location: location 21 | tags: tags 22 | logAnalyticsWorkspaceId: logAnalytics.outputs.id 23 | disableLocalAuth: disableLocalAuth 24 | } 25 | } 26 | 27 | output applicationInsightsConnectionString string = applicationInsights.outputs.connectionString 28 | output applicationInsightsInstrumentationKey string = applicationInsights.outputs.instrumentationKey 29 | output applicationInsightsName string = applicationInsights.outputs.name 30 | output logAnalyticsWorkspaceId string = logAnalytics.outputs.id 31 | output logAnalyticsWorkspaceName string = logAnalytics.outputs.name 32 | -------------------------------------------------------------------------------- /infra/core/storage/storage-account.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | param allowBlobPublicAccess bool = false 6 | param containers array = [] 7 | param kind string = 'StorageV2' 8 | param minimumTlsVersion string = 'TLS1_2' 9 | param sku object = { name: 'Standard_LRS' } 10 | param networkAcls object = { 11 | bypass: 'AzureServices' 12 | defaultAction: 'Allow' 13 | } 14 | 15 | resource storage 'Microsoft.Storage/storageAccounts@2023-05-01' = { 16 | name: name 17 | location: location 18 | tags: tags 19 | kind: kind 20 | sku: sku 21 | properties: { 22 | minimumTlsVersion: minimumTlsVersion 23 | allowBlobPublicAccess: allowBlobPublicAccess 24 | allowSharedKeyAccess: false 25 | networkAcls: networkAcls 26 | } 27 | 28 | resource blobServices 'blobServices' = if (!empty(containers)) { 29 | name: 'default' 30 | resource container 'containers' = [for container in containers: { 31 | name: container.name 32 | properties: { 33 | publicAccess: container.?publicAccess ?? 'None' 34 | } 35 | }] 36 | } 37 | } 38 | 39 | output name string = storage.name 40 | output primaryEndpoints object = storage.properties.primaryEndpoints 41 | output id string = storage.id 42 | -------------------------------------------------------------------------------- /infra/main.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'subscription' 2 | 3 | @minLength(1) 4 | @maxLength(64) 5 | @description('Name of the the environment which is used to generate a short unique hash used in all resources.') 6 | param environmentName string 7 | 8 | @minLength(1) 9 | @description('Primary location for all resources') 10 | @allowed(['australiaeast', 'eastasia', 'eastus', 'northeurope', 'southcentralus', 'southeastasia', 'uksouth', 'westus2']) 11 | @metadata({ 12 | azd: { 13 | type: 'location' 14 | } 15 | }) 16 | param location string 17 | param skipVnet bool = true 18 | param apiServiceName string = '' 19 | param apiUserAssignedIdentityName string = '' 20 | param applicationInsightsName string = '' 21 | param appServicePlanName string = '' 22 | param logAnalyticsName string = '' 23 | param resourceGroupName string = '' 24 | param storageAccountName string = '' 25 | param vNetName string = '' 26 | param disableLocalAuth bool = true 27 | param processedTextContainerName string = 'processed-text' 28 | param unprocessedPdfContainerName string = 'unprocessed-text' 29 | 30 | param aiResourceName string = '' 31 | 32 | @description('Id of the user or app to assign application roles') 33 | param principalId string = '' 34 | 35 | var abbrs = loadJsonContent('./abbreviations.json') 36 | var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) 37 | var tags = { 'azd-env-name': environmentName } 38 | var functionAppName = !empty(apiServiceName) ? apiServiceName : '${abbrs.webSitesFunctions}api-${resourceToken}' 39 | var deploymentStorageContainerName = 'app-package-${take(functionAppName, 32)}-${take(toLower(uniqueString(functionAppName, resourceToken)), 7)}' 40 | 41 | // Organize resources in a resource group 42 | resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { 43 | name: !empty(resourceGroupName) ? resourceGroupName : '${abbrs.resourcesResourceGroups}${environmentName}' 44 | location: location 45 | tags: tags 46 | } 47 | 48 | // User assigned managed identity to be used by the function app to reach storage and service bus 49 | module apiUserAssignedIdentity './core/identity/userAssignedIdentity.bicep' = { 50 | name: 'apiUserAssignedIdentity' 51 | scope: rg 52 | params: { 53 | location: location 54 | tags: tags 55 | identityName: !empty(apiUserAssignedIdentityName) ? apiUserAssignedIdentityName : '${abbrs.managedIdentityUserAssignedIdentities}api-${resourceToken}' 56 | } 57 | } 58 | 59 | module ai 'app/ai.bicep' = { 60 | name: 'ai' 61 | scope: rg 62 | params: { 63 | name: !empty(aiResourceName) ? aiResourceName : '${abbrs.cognitiveServicesTextAnalytics}-${resourceToken}' 64 | location: location 65 | tags: tags 66 | customSubDomainName: resourceToken 67 | } 68 | } 69 | 70 | // The application backend is a function app 71 | module appServicePlan './core/host/appserviceplan.bicep' = { 72 | name: 'appserviceplan' 73 | scope: rg 74 | params: { 75 | name: !empty(appServicePlanName) ? appServicePlanName : '${abbrs.webServerFarms}${resourceToken}' 76 | location: location 77 | tags: tags 78 | sku: { 79 | name: 'FC1' 80 | tier: 'FlexConsumption' 81 | } 82 | } 83 | } 84 | 85 | module api './app/api.bicep' = { 86 | name: 'api' 87 | scope: rg 88 | params: { 89 | name: functionAppName 90 | location: location 91 | tags: tags 92 | applicationInsightsName: monitoring.outputs.applicationInsightsName 93 | appServicePlanId: appServicePlan.outputs.id 94 | runtimeName: 'python' 95 | runtimeVersion: '3.11' 96 | storageAccountName: storage.outputs.name 97 | deploymentStorageContainerName: deploymentStorageContainerName 98 | identityId: apiUserAssignedIdentity.outputs.identityId 99 | identityClientId: apiUserAssignedIdentity.outputs.identityClientId 100 | appSettings: { 101 | } 102 | virtualNetworkSubnetId: skipVnet ? '' : serviceVirtualNetwork.outputs.appSubnetID 103 | aiLanguageServiceUrl: ai.outputs.url 104 | } 105 | } 106 | 107 | // Backing storage for Azure functions backend processor 108 | module storage 'core/storage/storage-account.bicep' = { 109 | name: 'storage' 110 | scope: rg 111 | params: { 112 | name: !empty(storageAccountName) ? storageAccountName : '${abbrs.storageStorageAccounts}${resourceToken}' 113 | location: location 114 | tags: tags 115 | containers: [ 116 | {name: deploymentStorageContainerName} 117 | {name: processedTextContainerName} 118 | {name: unprocessedPdfContainerName} 119 | ] 120 | networkAcls: skipVnet ? {} : { 121 | defaultAction: 'Deny' 122 | } 123 | } 124 | } 125 | 126 | module eventgripdftopic './app/eventgrid.bicep' = { 127 | name: 'eventgripdf' 128 | scope: rg 129 | params: { 130 | location: location 131 | tags: tags 132 | storageAccountId: storage.outputs.id 133 | } 134 | } 135 | 136 | var storageRoleDefinitionId = 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b' // Storage Blob Data Owner role 137 | 138 | // Allow access from api to storage account using a managed identity 139 | module storageRoleAssignmentApi 'app/storage-Access.bicep' = { 140 | name: 'storageRoleAssignmentapi' 141 | scope: rg 142 | params: { 143 | storageAccountName: storage.outputs.name 144 | roleDefinitionID: storageRoleDefinitionId 145 | principalID: apiUserAssignedIdentity.outputs.identityPrincipalId 146 | principalType: 'ServicePrincipal' 147 | } 148 | } 149 | 150 | module storageRoleAssignmentUserIdentityApi 'app/storage-Access.bicep' = { 151 | name: 'storageRoleAssignmentUserIdentityApi' 152 | scope: rg 153 | params: { 154 | storageAccountName: storage.outputs.name 155 | roleDefinitionID: storageRoleDefinitionId 156 | principalID: principalId 157 | principalType: 'User' 158 | } 159 | } 160 | 161 | var storageQueueDataContributorRoleDefinitionId = '974c5e8b-45b9-4653-ba55-5f855dd0fb88' // Storage Queue Data Contributor 162 | 163 | module storageQueueDataContributorRoleAssignmentprocessor 'app/storage-Access.bicep' = { 164 | name: 'storageQueueDataContributorRoleAssignmentprocessor' 165 | scope: rg 166 | params: { 167 | storageAccountName: storage.outputs.name 168 | roleDefinitionID: storageQueueDataContributorRoleDefinitionId 169 | principalID: apiUserAssignedIdentity.outputs.identityPrincipalId 170 | principalType: 'ServicePrincipal' 171 | } 172 | } 173 | 174 | module storageQueueDataContributorRoleAssignmentUserIdentityprocessor 'app/storage-Access.bicep' = { 175 | name: 'storageQueueDataContributorRoleAssignmentUserIdentityprocessor' 176 | scope: rg 177 | params: { 178 | storageAccountName: storage.outputs.name 179 | roleDefinitionID: storageQueueDataContributorRoleDefinitionId 180 | principalID: principalId 181 | principalType: 'User' 182 | } 183 | } 184 | 185 | var cogRoleDefinitionId = 'a97b65f3-24c7-4388-baec-2e87135dc908' // Cognitive Services User 186 | 187 | // Allow access from api to storage account using a managed identity 188 | module cogRoleAssignmentApi 'app/ai-Cog-Service-Access.bicep' = { 189 | name: 'cogRoleAssignmentapi' 190 | scope: rg 191 | params: { 192 | aiResourceName: ai.outputs.name 193 | roleDefinitionID: cogRoleDefinitionId 194 | principalID: apiUserAssignedIdentity.outputs.identityPrincipalId 195 | principalType: 'ServicePrincipal' 196 | } 197 | } 198 | 199 | module cogRoleAssignmentUserIdentityApi 'app/ai-Cog-Service-Access.bicep' = { 200 | name: 'cogRoleAssignmentUserIdentityApi' 201 | scope: rg 202 | params: { 203 | aiResourceName: ai.outputs.name 204 | roleDefinitionID: cogRoleDefinitionId 205 | principalID: principalId 206 | principalType: 'User' 207 | } 208 | } 209 | 210 | // Virtual Network & private endpoint to blob storage 211 | module serviceVirtualNetwork 'app/vnet.bicep' = if (!skipVnet) { 212 | name: 'serviceVirtualNetwork' 213 | scope: rg 214 | params: { 215 | location: location 216 | tags: tags 217 | vNetName: !empty(vNetName) ? vNetName : '${abbrs.networkVirtualNetworks}${resourceToken}' 218 | } 219 | } 220 | 221 | module storagePrivateEndpoint 'app/storage-PrivateEndpoint.bicep' = if (!skipVnet) { 222 | name: 'servicePrivateEndpoint' 223 | scope: rg 224 | params: { 225 | location: location 226 | tags: tags 227 | virtualNetworkName: !empty(vNetName) ? vNetName : '${abbrs.networkVirtualNetworks}${resourceToken}' 228 | subnetName: skipVnet ? '' : serviceVirtualNetwork.outputs.peSubnetName 229 | resourceName: storage.outputs.name 230 | } 231 | } 232 | 233 | // Monitor application with Azure Monitor 234 | module monitoring './core/monitor/monitoring.bicep' = { 235 | name: 'monitoring' 236 | scope: rg 237 | params: { 238 | location: location 239 | tags: tags 240 | logAnalyticsName: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' 241 | applicationInsightsName: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' 242 | disableLocalAuth: disableLocalAuth 243 | } 244 | } 245 | 246 | var monitoringRoleDefinitionId = '3913510d-42f4-4e42-8a64-420c390055eb' // Monitoring Metrics Publisher role ID 247 | 248 | // Allow access from api to application insights using a managed identity 249 | module appInsightsRoleAssignmentApi './core/monitor/appinsights-access.bicep' = { 250 | name: 'appInsightsRoleAssignmentapi' 251 | scope: rg 252 | params: { 253 | appInsightsName: monitoring.outputs.applicationInsightsName 254 | roleDefinitionID: monitoringRoleDefinitionId 255 | principalID: apiUserAssignedIdentity.outputs.identityPrincipalId 256 | } 257 | } 258 | 259 | // App outputs 260 | output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString 261 | output AZURE_LOCATION string = location 262 | output AZURE_TENANT_ID string = tenant().tenantId 263 | output SERVICE_API_NAME string = api.outputs.SERVICE_API_NAME 264 | output AZURE_FUNCTION_APP_NAME string = api.outputs.SERVICE_API_NAME 265 | output RESOURCE_GROUP string = rg.name 266 | output UNPROCESSED_PDF_CONTAINER_NAME string = unprocessedPdfContainerName 267 | output UNPROCESSED_PDF_SYSTEM_TOPIC_NAME string = eventgripdftopic.outputs.unprocessedPdfSystemTopicName 268 | output TEXT_ANALYTICS_ENDPOINT string = ai.outputs.url 269 | -------------------------------------------------------------------------------- /infra/main.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "environmentName": { 6 | "value": "${AZURE_ENV_NAME}" 7 | }, 8 | "location": { 9 | "value": "${AZURE_LOCATION}" 10 | }, 11 | "skipVnet": { 12 | "value": "${SKIP_VNET}" 13 | }, 14 | "principalId": { 15 | "value": "${AZURE_PRINCIPAL_ID}" 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /scripts/post-up.ps1: -------------------------------------------------------------------------------- 1 | # Paramters: 2 | # Set function_name value to match code that was deployed 3 | $function_name="summarize_function" 4 | 5 | # Check for pre-requisites 6 | $tools = @("az") 7 | 8 | foreach ($tool in $tools) { 9 | if (!(Get-Command $tool -ErrorAction SilentlyContinue)) { 10 | Write-Host "Error: $tool command line tool is not available, check pre-requisites in README.md" 11 | exit 1 12 | } 13 | } 14 | 15 | # Get the function blobs_extension key 16 | $blobs_extension=$(az functionapp keys list -n ${env:AZURE_FUNCTION_APP_NAME} -g ${env:RESOURCE_GROUP} --query "systemKeys.blobs_extension" -o tsv) 17 | 18 | # Build the endpoint URL with the function name and extension key and create the event subscription 19 | # Double quotes added here to allow the az command to work successfully. Quoting inside az command had issues. 20 | $endpointUrl="""https://" + ${env:AZURE_FUNCTION_APP_NAME} + ".azurewebsites.net/runtime/webhooks/blobs?functionName=Host.Functions." + $function_name + "&code=" + $blobs_extension + """" 21 | 22 | $filter="/blobServices/default/containers/" + ${env:UNPROCESSED_PDF_CONTAINER_NAME} 23 | 24 | az eventgrid system-topic event-subscription create -n unprocessed-pdf-topic-subscription -g ${env:RESOURCE_GROUP} --system-topic-name ${env:UNPROCESSED_PDF_SYSTEM_TOPIC_NAME} --endpoint-type webhook --endpoint $endpointUrl --included-event-types Microsoft.Storage.BlobCreated --subject-begins-with $filter 25 | 26 | Write-Output "Created blob event grid subscription successfully." 27 | -------------------------------------------------------------------------------- /scripts/post-up.sh: -------------------------------------------------------------------------------- 1 | # Parameters: 2 | # Set function_name value to match code that was deployed 3 | function_name="summarize_function" 4 | 5 | # Check for pre-requisites 6 | commands=("az") 7 | 8 | for cmd in "${commands[@]}"; do 9 | if ! command -v "$cmd" &>/dev/null; then 10 | echo "Error: $cmd command is not available, check pre-requisites in README.md" 11 | exit 1 12 | fi 13 | done 14 | 15 | # Get the function blobs_extension key 16 | blobs_extension=$(az functionapp keys list -n ${AZURE_FUNCTION_APP_NAME} -g ${RESOURCE_GROUP} --query "systemKeys.blobs_extension" -o tsv) 17 | 18 | # Build the endpoint URL with the function name and extension key and create the event subscription 19 | endpointUrl="https://${AZURE_FUNCTION_APP_NAME}.azurewebsites.net/runtime/webhooks/blobs?functionName=Host.Functions.${function_name}&code=${blobs_extension}" 20 | filter="/blobServices/default/containers/${UNPROCESSED_PDF_CONTAINER_NAME}" 21 | 22 | echo "az eventgrid system-topic event-subscription create -n "unprocessed-pdf-topic-subscription" -g "${RESOURCE_GROUP}" --system-topic-name "${UNPROCESSED_PDF_SYSTEM_TOPIC_NAME}" --endpoint-type "webhook" --endpoint "$endpointUrl" --included-event-types "Microsoft.Storage.BlobCreated" --subject-begins-with "$filter"" 23 | az eventgrid system-topic event-subscription create -n "unprocessed-pdf-topic-subscription" -g "${RESOURCE_GROUP}" --system-topic-name "${UNPROCESSED_PDF_SYSTEM_TOPIC_NAME}" --endpoint-type "webhook" --endpoint "$endpointUrl" --included-event-types "Microsoft.Storage.BlobCreated" --subject-begins-with "$filter" 24 | 25 | echo "Created blob event grid subscription successfully." 26 | -------------------------------------------------------------------------------- /text_summarize/.funcignore: -------------------------------------------------------------------------------- 1 | .venv -------------------------------------------------------------------------------- /text_summarize/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | csx 4 | .vs 5 | edge 6 | Publish 7 | 8 | *.user 9 | *.suo 10 | *.cscfg 11 | *.Cache 12 | project.lock.json 13 | 14 | /packages 15 | /TestResults 16 | 17 | /tools/NuGet.exe 18 | /App_Data 19 | /secrets 20 | /data 21 | .secrets 22 | appsettings.json 23 | local.settings.json 24 | 25 | node_modules 26 | dist 27 | 28 | # Local python packages 29 | .python_packages/ 30 | 31 | # Python Environments 32 | .env 33 | .venv 34 | env/ 35 | venv/ 36 | ENV/ 37 | env.bak/ 38 | venv.bak/ 39 | 40 | # Byte-compiled / optimized / DLL files 41 | __pycache__/ 42 | *.py[cod] 43 | *$py.class 44 | 45 | # Azurite artifacts 46 | __blobstorage__ 47 | __queuestorage__ 48 | __azurite_db*__.json -------------------------------------------------------------------------------- /text_summarize/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions", 4 | "ms-python.python" 5 | ] 6 | } -------------------------------------------------------------------------------- /text_summarize/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach to Python Functions", 6 | "type": "python", 7 | "request": "attach", 8 | "port": 9091, 9 | "preLaunchTask": "func: host start" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /text_summarize/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "azureFunctions.deploySubpath": ".", 3 | "azureFunctions.scmDoBuildDuringDeployment": true, 4 | "azureFunctions.pythonVenv": ".venv", 5 | "azureFunctions.projectLanguage": "Python", 6 | "azureFunctions.projectRuntime": "~4", 7 | "debug.internalConsoleOptions": "neverOpen", 8 | "azureFunctions.projectLanguageModel": 2 9 | } -------------------------------------------------------------------------------- /text_summarize/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "func", 6 | "label": "func: host start", 7 | "command": "host start", 8 | "problemMatcher": "$func-python-watch", 9 | "isBackground": true, 10 | "dependsOn": "pip install (functions)" 11 | }, 12 | { 13 | "label": "pip install (functions)", 14 | "type": "shell", 15 | "osx": { 16 | "command": "${config:azureFunctions.pythonVenv}/bin/python -m pip install -r requirements.txt" 17 | }, 18 | "windows": { 19 | "command": "${config:azureFunctions.pythonVenv}\\Scripts\\python -m pip install -r requirements.txt" 20 | }, 21 | "linux": { 22 | "command": "${config:azureFunctions.pythonVenv}/bin/python -m pip install -r requirements.txt" 23 | }, 24 | "problemMatcher": [] 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /text_summarize/function_app.py: -------------------------------------------------------------------------------- 1 | import azure.functions as func 2 | import logging 3 | from azure.ai.textanalytics import TextAnalyticsClient 4 | from azure.identity import DefaultAzureCredential 5 | import os 6 | 7 | app = func.FunctionApp() 8 | 9 | # Load AI url and secrets from Env Variables in Terminal before running, 10 | # e.g. `export TEXT_ANALYTICS_ENDPOINT=https://.cognitiveservices.azure.com/` 11 | endpoint = os.getenv('TEXT_ANALYTICS_ENDPOINT', 'SETENVVAR!') 12 | 13 | # Create client using Entra User or Managed Identity (no longer AzureKeyCredential) 14 | # This requires a sub domain name to be set in endpoint URL for Managed Identity support 15 | # See https://learn.microsoft.com/en-us/azure/ai-services/authentication#authenticate-with-microsoft-entra-id 16 | text_analytics_client = TextAnalyticsClient( 17 | endpoint=endpoint, 18 | credential=DefaultAzureCredential(), 19 | ) 20 | 21 | @app.function_name(name="summarize_function") 22 | @app.blob_trigger(arg_name="myblob", path="unprocessed-text/{name}", 23 | connection="AzureWebJobsStorage", source="EventGrid") 24 | @app.blob_output(arg_name="outputblob", path="processed-text/{name}-output.txt", connection="AzureWebJobsStorage") 25 | def test_function(myblob: func.InputStream, outputblob: func.Out[str]): 26 | logging.info(f"Triggered item: {myblob.name}\n") 27 | 28 | document = [myblob.read().decode('utf-8')] 29 | summarized_text = ai_summarize_txt(document) 30 | logging.info(f"\n *****Summary***** \n{summarized_text}"); 31 | outputblob.set(summarized_text) 32 | 33 | # Example method for summarizing text 34 | def ai_summarize_txt(document): 35 | 36 | poller = text_analytics_client.begin_extract_summary(document) 37 | extract_summary_results = poller.result() 38 | 39 | summarized_text = "" 40 | document_results = poller.result() 41 | for result in extract_summary_results: 42 | if result.kind == "ExtractiveSummarization": 43 | summarized_text= "Summary extracted: \n{}".format( 44 | " ".join([sentence.text for sentence in result.sentences])) 45 | print(summarized_text) 46 | logging.info(f"Returning summarized text: \n{summarized_text}") 47 | elif result.is_error is True: 48 | print("...Is an error with code '{}' and message '{}'".format( 49 | result.error.code, result.error.message 50 | )) 51 | logging.error(f"Error with code '{result.error.code}' and message '{result.error.message}'") 52 | 53 | # Perform sentiment analysis on document summary 54 | sentiment_result = text_analytics_client.analyze_sentiment([summarized_text])[0] 55 | print(f"\nSentiment: {sentiment_result.sentiment}") 56 | print(f"Positive Score: {sentiment_result.confidence_scores.positive}") 57 | print(f"Negative Score: {sentiment_result.confidence_scores.negative}") 58 | print(f"Neutral Score: {sentiment_result.confidence_scores.neutral}") 59 | 60 | summary_with_sentiment = summarized_text + f"\nSentiment: {sentiment_result.sentiment}\n" 61 | 62 | return summary_with_sentiment 63 | -------------------------------------------------------------------------------- /text_summarize/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.0.0, 5.0.0)" 14 | }, 15 | "concurrency": { 16 | "dynamicConcurrencyEnabled": true, 17 | "snapshotPersistenceEnabled": true 18 | } 19 | } -------------------------------------------------------------------------------- /text_summarize/requirements.txt: -------------------------------------------------------------------------------- 1 | # Do not include azure-functions-worker in this file 2 | # The Python Worker is managed by the Azure Functions platform 3 | # Manually managing azure-functions-worker may cause unexpected issues 4 | 5 | azure-functions 6 | azure-ai-textanalytics 7 | azure-identity 8 | --------------------------------------------------------------------------------