Skip to content

Commit 35052c8

Browse files
author
Ian Seabock (Centific Technologies Inc)
committed
merge
2 parents fc474c9 + 663e920 commit 35052c8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+22731
-6424
lines changed

.env.sample

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,5 +106,6 @@ USE_PROMPTFLOW=False
106106
PROMPTFLOW_ENDPOINT=
107107
PROMPTFLOW_API_KEY=
108108
PROMPTFLOW_RESPONSE_TIMEOUT=120
109-
PROMPTFLOW_REQUEST_FIELD_NAME=question
110-
PROMPTFLOW_RESPONSE_FIELD_NAME=answer
109+
PROMPTFLOW_REQUEST_FIELD_NAME=query
110+
PROMPTFLOW_RESPONSE_FIELD_NAME=reply
111+
PROMPTFLOW_CITATIONS_FIELD_NAME=documents

.github/workflows/node.js.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,4 @@ jobs:
3131
cache-dependency-path: '**/package-lock.json'
3232
- run: npm ci
3333
- run: npm run build --if-present
34-
# - run: npm test
34+
- run: npm run test --if-present

.github/workflows/python-app.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# This workflow will install Python dependencies, run tests and lint with a single version of Python
2+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
3+
4+
name: Python application
5+
6+
on:
7+
push:
8+
branches: [ "main" ]
9+
pull_request:
10+
branches: [ "main" ]
11+
12+
permissions:
13+
contents: read
14+
15+
jobs:
16+
build:
17+
18+
runs-on: ubuntu-latest
19+
20+
steps:
21+
- uses: actions/checkout@v3
22+
- name: Set up Python 3.11
23+
uses: actions/setup-python@v3
24+
with:
25+
python-version: "3.11"
26+
- name: Install dependencies
27+
run: |
28+
python -m pip install --upgrade pip
29+
pip install pytest
30+
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
31+
- name: Test with pytest
32+
run: |
33+
export PYTHONPATH=$(pwd)
34+
pytest -v --show-capture=stdout
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
2+
# More GitHub Actions for Azure: https://github.com/Azure/actions
3+
# More info on Python, GitHub Actions, and Azure App Service: https://aka.ms/python-webapps-actions
4+
5+
name: Build and deploy Python app to Azure Web App - sample-app-github-cd
6+
7+
on:
8+
push:
9+
branches:
10+
- main
11+
workflow_dispatch:
12+
13+
jobs:
14+
build:
15+
runs-on: ubuntu-latest
16+
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- name: Set up Python version
21+
uses: actions/setup-python@v1
22+
with:
23+
python-version: '3.11'
24+
25+
- name: Create and start virtual environment
26+
run: |
27+
python -m venv venv
28+
source venv/bin/activate
29+
30+
- name: Install dependencies
31+
run: pip install -r requirements.txt
32+
33+
# Optional: Add step to run tests here (PyTest, Django test suites, etc.)
34+
35+
- name: Zip artifact for deployment
36+
run: zip release.zip ./* -r
37+
38+
- name: Upload artifact for deployment jobs
39+
uses: actions/upload-artifact@v3
40+
with:
41+
name: python-app
42+
path: |
43+
release.zip
44+
!venv/
45+
46+
deploy:
47+
runs-on: ubuntu-latest
48+
needs: build
49+
environment:
50+
name: 'Production'
51+
url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}
52+
permissions:
53+
id-token: write #This is required for requesting the JWT
54+
55+
steps:
56+
- name: Download artifact from build job
57+
uses: actions/download-artifact@v3
58+
with:
59+
name: python-app
60+
61+
- name: Unzip artifact for deployment
62+
run: unzip release.zip
63+
64+
65+
- name: Login to Azure
66+
uses: azure/login@v1
67+
with:
68+
client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_70EAEB01E7344A70ADC936904D8668C0 }}
69+
tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_DE1C873329344453B852433BF700723B }}
70+
subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_07FF4A7ACD624D90ADE700FA7786CF46 }}
71+
72+
- name: 'Deploy to Azure Web App'
73+
uses: azure/webapps-deploy@v2
74+
id: deploy-to-webapp
75+
with:
76+
app-name: 'sample-app-github-cd'
77+
slot-name: 'Production'
78+

README.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,8 +236,9 @@ Note: settings starting with `AZURE_SEARCH` are only needed when using Azure Ope
236236
|PROMPTFLOW_ENDPOINT||URL of the deployed Promptflow endpoint e.g. https://pf-deployment-name.region.inference.ml.azure.com/score|
237237
|PROMPTFLOW_API_KEY||Auth key for deployed Promptflow endpoint. Note: only Key-based authentication is supported.|
238238
|PROMPTFLOW_RESPONSE_TIMEOUT|120|Timeout value in seconds for the Promptflow endpoint to respond.|
239-
|PROMPTFLOW_REQUEST_FIELD_NAME|question|Default field name to construct Promptflow request. Note: chat_history is auto constucted based on the interaction, if your API expects other mandatory field you will need to change the request parameters under `promptflow_request` function.|
240-
|PROMPTFLOW_RESPONSE_FIELD_NAME|answer|Default field name to process the response from Promptflow request.|
239+
|PROMPTFLOW_REQUEST_FIELD_NAME|query|Default field name to construct Promptflow request. Note: chat_history is auto constucted based on the interaction, if your API expects other mandatory field you will need to change the request parameters under `promptflow_request` function.|
240+
|PROMPTFLOW_RESPONSE_FIELD_NAME|reply|Default field name to process the response from Promptflow request.|
241+
|PROMPTFLOW_CITATIONS_FIELD_NAME|documents|Default field name to process the citations output from Promptflow request.|
241242

242243
## Contributing
243244

@@ -253,6 +254,22 @@ This project has adopted the [Microsoft Open Source Code of Conduct](https://ope
253254
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
254255
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
255256

257+
When contributing to this repository, please help keep the codebase clean and maintainable by running
258+
the formatter and linter with `npm run format` this will run `npx eslint --fix` and `npx prettier --write`
259+
on the frontebnd codebase.
260+
261+
If you are using VSCode, you can add the following settings to your `settings.json` to format and lint on save:
262+
263+
```json
264+
{
265+
"editor.codeActionsOnSave": {
266+
"source.fixAll.eslint": "explicit"
267+
},
268+
"editor.formatOnSave": true,
269+
"prettier.requireConfig": true,
270+
}
271+
```
272+
256273
## Trademarks
257274

258275
This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft

app.py

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
from openai import AsyncAzureOpenAI
1919
from azure.identity.aio import DefaultAzureCredential, get_bearer_token_provider
20-
from backend.auth.auth_utils import get_authenticated_user_details
20+
from backend.auth.auth_utils import get_authenticated_user_details, get_tenantid
2121
from backend.history.cosmosdbservice import CosmosConversationClient
2222

2323
from backend.utils import (
@@ -244,6 +244,9 @@ async def assets(path):
244244
PROMPTFLOW_RESPONSE_FIELD_NAME = os.environ.get(
245245
"PROMPTFLOW_RESPONSE_FIELD_NAME", "reply"
246246
)
247+
PROMPTFLOW_CITATIONS_FIELD_NAME = os.environ.get(
248+
"PROMPTFLOW_CITATIONS_FIELD_NAME", "documents"
249+
)
247250
# Frontend Settings via Environment Variables
248251
AUTH_ENABLED = os.environ.get("AUTH_ENABLED", "true").lower() == "true"
249252
CHAT_HISTORY_ENABLED = (
@@ -266,7 +269,8 @@ async def assets(path):
266269
},
267270
"sanitize_answer": SANITIZE_ANSWER,
268271
}
269-
272+
# Enable Microsoft Defender for Cloud Integration
273+
MS_DEFENDER_ENABLED = os.environ.get("MS_DEFENDER_ENABLED", "false").lower() == "true"
270274

271275
def should_use_data():
272276
global DATASOURCE_TYPE
@@ -722,7 +726,7 @@ def get_configured_data_source():
722726
return data_source
723727

724728

725-
def prepare_model_args(request_body):
729+
def prepare_model_args(request_body, request_headers):
726730
request_messages = request_body.get("messages", [])
727731
messages = []
728732
if not SHOULD_USE_DATA:
@@ -732,6 +736,20 @@ def prepare_model_args(request_body):
732736
if message:
733737
messages.append({"role": message["role"], "content": message["content"]})
734738

739+
user_json = None
740+
if (MS_DEFENDER_ENABLED):
741+
authenticated_user_details = get_authenticated_user_details(request_headers)
742+
tenantId = get_tenantid(authenticated_user_details.get("client_principal_b64"))
743+
conversation_id = request_body.get("conversation_id", None)
744+
user_args = {
745+
"EndUserId": authenticated_user_details.get('user_principal_id'),
746+
"EndUserIdType": 'Entra',
747+
"EndUserTenantId": tenantId,
748+
"ConversationId": conversation_id,
749+
"SourceIp": request_headers.get('X-Forwarded-For', request_headers.get('Remote-Addr', '')),
750+
}
751+
user_json = json.dumps(user_args)
752+
735753
model_args = {
736754
"messages": messages,
737755
"temperature": float(AZURE_OPENAI_TEMPERATURE),
@@ -744,6 +762,7 @@ def prepare_model_args(request_body):
744762
),
745763
"stream": SHOULD_STREAM,
746764
"model": AZURE_OPENAI_MODEL,
765+
"user": user_json,
747766
}
748767

749768
if SHOULD_USE_DATA:
@@ -821,56 +840,62 @@ async def promptflow_request(request):
821840
logging.error(f"An error occurred while making promptflow_request: {e}")
822841

823842

824-
async def send_chat_request(request):
825-
filtered_messages = [message for message in request['messages'] if message['role'] != 'tool']
826-
request['messages'] = filtered_messages
827-
model_args = prepare_model_args(request)
843+
async def send_chat_request(request_body, request_headers):
844+
filtered_messages = []
845+
messages = request_body.get("messages", [])
846+
for message in messages:
847+
if message.get("role") != 'tool':
848+
filtered_messages.append(message)
849+
850+
request_body['messages'] = filtered_messages
851+
model_args = prepare_model_args(request_body, request_headers)
828852

829853
try:
830854
azure_openai_client = init_openai_client()
831-
response = await azure_openai_client.chat.completions.create(**model_args)
832-
855+
raw_response = await azure_openai_client.chat.completions.with_raw_response.create(**model_args)
856+
response = raw_response.parse()
857+
apim_request_id = raw_response.headers.get("apim-request-id")
833858
except Exception as e:
834859
logging.exception("Exception in send_chat_request")
835860
raise e
836861

837-
return response
862+
return response, apim_request_id
838863

839864

840-
async def complete_chat_request(request_body):
865+
async def complete_chat_request(request_body, request_headers):
841866
if USE_PROMPTFLOW and PROMPTFLOW_ENDPOINT and PROMPTFLOW_API_KEY:
842867
response = await promptflow_request(request_body)
843868
history_metadata = request_body.get("history_metadata", {})
844869
return format_pf_non_streaming_response(
845-
response, history_metadata, PROMPTFLOW_RESPONSE_FIELD_NAME
870+
response, history_metadata, PROMPTFLOW_RESPONSE_FIELD_NAME, PROMPTFLOW_CITATIONS_FIELD_NAME
846871
)
847872
else:
848-
response = await send_chat_request(request_body)
873+
response, apim_request_id = await send_chat_request(request_body, request_headers)
849874
history_metadata = request_body.get("history_metadata", {})
850-
return format_non_streaming_response(response, history_metadata)
875+
return format_non_streaming_response(response, history_metadata, apim_request_id)
851876

852877

853-
async def stream_chat_request(request_body):
854-
response = await send_chat_request(request_body)
878+
async def stream_chat_request(request_body, request_headers):
879+
response, apim_request_id = await send_chat_request(request_body, request_headers)
855880
history_metadata = request_body.get("history_metadata", {})
856-
881+
857882
async def generate():
858883
async for completionChunk in response:
859-
yield format_stream_response(completionChunk, history_metadata)
884+
yield format_stream_response(completionChunk, history_metadata, apim_request_id)
860885

861886
return generate()
862887

863888

864-
async def conversation_internal(request_body):
889+
async def conversation_internal(request_body, request_headers):
865890
try:
866891
if SHOULD_STREAM:
867-
result = await stream_chat_request(request_body)
892+
result = await stream_chat_request(request_body, request_headers)
868893
response = await make_response(format_as_ndjson(result))
869894
response.timeout = None
870895
response.mimetype = "application/json-lines"
871896
return response
872897
else:
873-
result = await complete_chat_request(request_body)
898+
result = await complete_chat_request(request_body, request_headers)
874899
return jsonify(result)
875900

876901
except Exception as ex:
@@ -887,7 +912,7 @@ async def conversation():
887912
return jsonify({"error": "request must be json"}), 415
888913
request_json = await request.get_json()
889914

890-
return await conversation_internal(request_json)
915+
return await conversation_internal(request_json, request.headers)
891916

892917

893918
@bp.route("/frontend_settings", methods=["GET"])
@@ -951,7 +976,7 @@ async def add_conversation():
951976
request_body = await request.get_json()
952977
history_metadata["conversation_id"] = conversation_id
953978
request_body["history_metadata"] = history_metadata
954-
return await conversation_internal(request_body)
979+
return await conversation_internal(request_body, request.headers)
955980

956981
except Exception as e:
957982
logging.exception("Exception in /history/generate")

backend/auth/auth_utils.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import base64
2+
import json
3+
import logging
4+
15
def get_authenticated_user_details(request_headers):
26
user_object = {}
37

@@ -17,4 +21,19 @@ def get_authenticated_user_details(request_headers):
1721
user_object['client_principal_b64'] = raw_user_object.get('X-Ms-Client-Principal')
1822
user_object['aad_id_token'] = raw_user_object.get('X-Ms-Token-Aad-Id-Token')
1923

20-
return user_object
24+
return user_object
25+
26+
def get_tenantid(client_principal_b64):
27+
tenant_id = ''
28+
if client_principal_b64:
29+
try:
30+
# Decode the base64 header to get the JSON string
31+
decoded_bytes = base64.b64decode(client_principal_b64)
32+
decoded_string = decoded_bytes.decode('utf-8')
33+
# Convert the JSON string1into a Python dictionary
34+
user_info = json.loads(decoded_string)
35+
# Extract the tenant ID
36+
tenant_id = user_info.get('tid') # 'tid' typically holds the tenant ID
37+
except Exception as ex:
38+
logging.exception(ex)
39+
return tenant_id

0 commit comments

Comments
 (0)