├── .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 | [](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 |
--------------------------------------------------------------------------------