Skip to content

Commit 6e7beec

Browse files
authored
Merge branch 'main' into main
2 parents e32ea9b + ef32ced commit 6e7beec

17 files changed

+96
-55
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,14 @@ You can run this repo virtually by using GitHub Codespaces or VS Code Remote Con
5353
1. Create a new folder and switch to it in the terminal
5454
1. Run `azd auth login`
5555
1. Run `azd init -t azure-search-openai-demo`
56-
* For the target location, the regions that currently support the models used in this sample are **East US** or **South Central US**. For an up-to-date list of regions and models, check [here](https://learn.microsoft.com/en-us/azure/cognitive-services/openai/concepts/models)
5756
* note that this command will initialize a git repository and you do not need to clone this repository
5857

5958
#### Starting from scratch
6059

6160
Execute the following command, if you don't have any pre-existing Azure services and want to start from a fresh deployment.
6261

6362
1. Run `azd up` - This will provision Azure resources and deploy this sample to those resources, including building the search index based on the files found in the `./data` folder.
63+
* For the target location, the regions that currently support the models used in this sample are **East US**, **France Central**, **South Central US**, **UK South**, and **West Europe**. For an up-to-date list of regions and models, check [here](https://learn.microsoft.com/en-us/azure/cognitive-services/openai/concepts/models)
6464
1. After the application has been successfully deployed you will see a URL printed to the console. Click that URL to interact with the application in your browser.
6565

6666
It will look like the following:
@@ -129,4 +129,4 @@ Once in the web app:
129129

130130
If you see this error while running `azd deploy`: `read /tmp/azd1992237260/backend_env/lib64: is a directory`, then delete the `./app/backend/backend_env folder` and re-run the `azd deploy` command. This issue is being tracked here: <https://github.com/Azure/azure-dev/issues/1237>
131131

132-
If the web app fails to deploy and you receive a '404 Not Found' message in your browser, run 'azd deploy'.
132+
If the web app fails to deploy and you receive a '404 Not Found' message in your browser, run `azd deploy`.

app/backend/app.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import os
2+
import io
23
import mimetypes
34
import time
45
import logging
56
import openai
6-
from flask import Flask, request, jsonify
7+
from flask import Flask, request, jsonify, send_file, abort
78
from azure.identity import DefaultAzureCredential
89
from azure.search.documents import SearchClient
910
from approaches.retrievethenread import RetrieveThenReadApproach
@@ -76,14 +77,21 @@ def static_file(path):
7677
@app.route("/content/<path>")
7778
def content_file(path):
7879
blob = blob_container.get_blob_client(path).download_blob()
80+
if not blob.properties or not blob.properties.has_key("content_settings"):
81+
abort(404)
7982
mime_type = blob.properties["content_settings"]["content_type"]
8083
if mime_type == "application/octet-stream":
8184
mime_type = mimetypes.guess_type(path)[0] or "application/octet-stream"
82-
return blob.readall(), 200, {"Content-Type": mime_type, "Content-Disposition": f"inline; filename={path}"}
85+
blob_file = io.BytesIO()
86+
blob.readinto(blob_file)
87+
blob_file.seek(0)
88+
return send_file(blob_file, mimetype=mime_type, as_attachment=False, download_name=path)
8389

8490
@app.route("/ask", methods=["POST"])
8591
def ask():
8692
ensure_openai_token()
93+
if not request.json:
94+
return jsonify({"error": "request must be json"}), 400
8795
approach = request.json["approach"]
8896
try:
8997
impl = ask_approaches.get(approach)
@@ -98,6 +106,8 @@ def ask():
98106
@app.route("/chat", methods=["POST"])
99107
def chat():
100108
ensure_openai_token()
109+
if not request.json:
110+
return jsonify({"error": "request must be json"}), 400
101111
approach = request.json["approach"]
102112
try:
103113
impl = chat_approaches.get(approach)

app/backend/approaches/approach.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from typing import Any
2+
3+
14
class Approach:
2-
def run(self, q: str, use_summaries: bool) -> any:
5+
def run(self, q: str, overrides: dict[str, Any]) -> Any:
36
raise NotImplementedError

app/backend/approaches/chatreadretrieveread.py

+13-8
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
1+
from typing import Any, Sequence
2+
13
import openai
24
from azure.search.documents import SearchClient
35
from azure.search.documents.models import QueryType
46
from approaches.approach import Approach
57
from text import nonewlines
68

7-
# Simple retrieve-then-read implementation, using the Cognitive Search and OpenAI APIs directly. It first retrieves
8-
# top documents from search, then constructs a prompt with them, and then uses OpenAI to generate an completion
9-
# (answer) with that prompt.
109
class ChatReadRetrieveReadApproach(Approach):
10+
"""
11+
Simple retrieve-then-read implementation, using the Cognitive Search and OpenAI APIs directly. It first retrieves
12+
top documents from search, then constructs a prompt with them, and then uses OpenAI to generate an completion
13+
(answer) with that prompt.
14+
"""
15+
1116
prompt_prefix = """<|im_start|>system
1217
Assistant helps the company employees with their healthcare plan questions, and questions about the employee handbook. Be brief in your answers.
1318
Answer ONLY with the facts listed in the list of sources below. If there isn't enough information below, say you don't know. Do not generate answers that don't use the sources below. If asking a clarifying question to the user would help, ask the question.
1419
For tabular information return it as an html table. Do not return markdown format.
15-
Each source has a name followed by colon and the actual information, always include the source name for each fact you use in the response. Use square brakets to reference the source, e.g. [info1.txt]. Don't combine sources, list each source separately, e.g. [info1.txt][info2.pdf].
20+
Each source has a name followed by colon and the actual information, always include the source name for each fact you use in the response. Use square brackets to reference the source, e.g. [info1.txt]. Don't combine sources, list each source separately, e.g. [info1.txt][info2.pdf].
1621
{follow_up_questions_prompt}
1722
{injected_prompt}
1823
Sources:
@@ -48,7 +53,7 @@ def __init__(self, search_client: SearchClient, chatgpt_deployment: str, gpt_dep
4853
self.sourcepage_field = sourcepage_field
4954
self.content_field = content_field
5055

51-
def run(self, history: list[dict], overrides: dict) -> any:
56+
def run(self, history: Sequence[dict[str, str]], overrides: dict[str, Any]) -> Any:
5257
use_semantic_captions = True if overrides.get("semantic_captions") else False
5358
top = overrides.get("top") or 3
5459
exclude_category = overrides.get("exclude_category") or None
@@ -105,10 +110,10 @@ def run(self, history: list[dict], overrides: dict) -> any:
105110

106111
return {"data_points": results, "answer": completion.choices[0].text, "thoughts": f"Searched for:<br>{q}<br><br>Prompt:<br>" + prompt.replace('\n', '<br>')}
107112

108-
def get_chat_history_as_text(self, history, include_last_turn=True, approx_max_tokens=1000) -> str:
113+
def get_chat_history_as_text(self, history: Sequence[dict[str, str]], include_last_turn: bool=True, approx_max_tokens: int=1000) -> str:
109114
history_text = ""
110115
for h in reversed(history if include_last_turn else history[:-1]):
111-
history_text = """<|im_start|>user""" +"\n" + h["user"] + "\n" + """<|im_end|>""" + "\n" + """<|im_start|>assistant""" + "\n" + (h.get("bot") + """<|im_end|>""" if h.get("bot") else "") + "\n" + history_text
116+
history_text = """<|im_start|>user""" + "\n" + h["user"] + "\n" + """<|im_end|>""" + "\n" + """<|im_start|>assistant""" + "\n" + (h.get("bot", "") + """<|im_end|>""" if h.get("bot") else "") + "\n" + history_text
112117
if len(history_text) > approx_max_tokens*4:
113118
break
114-
return history_text
119+
return history_text

app/backend/approaches/readdecomposeask.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from langchain.agents.react.base import ReActDocstoreAgent
1111
from langchainadapters import HtmlCallbackHandler
1212
from text import nonewlines
13-
from typing import List
13+
from typing import Any, List, Optional
1414

1515
class ReadDecomposeAsk(Approach):
1616
def __init__(self, search_client: SearchClient, openai_deployment: str, sourcepage_field: str, content_field: str):
@@ -19,7 +19,7 @@ def __init__(self, search_client: SearchClient, openai_deployment: str, sourcepa
1919
self.sourcepage_field = sourcepage_field
2020
self.content_field = content_field
2121

22-
def search(self, q: str, overrides: dict) -> str:
22+
def search(self, q: str, overrides: dict[str, Any]) -> str:
2323
use_semantic_captions = True if overrides.get("semantic_captions") else False
2424
top = overrides.get("top") or 3
2525
exclude_category = overrides.get("exclude_category") or None
@@ -42,7 +42,7 @@ def search(self, q: str, overrides: dict) -> str:
4242
self.results = [doc[self.sourcepage_field] + ":" + nonewlines(doc[self.content_field][:500]) for doc in r]
4343
return "\n".join(self.results)
4444

45-
def lookup(self, q: str) -> str:
45+
def lookup(self, q: str) -> Optional[str]:
4646
r = self.search_client.search(q,
4747
top = 1,
4848
include_total_count=True,
@@ -58,9 +58,9 @@ def lookup(self, q: str) -> str:
5858
return answers[0].text
5959
if r.get_count() > 0:
6060
return "\n".join(d['content'] for d in r)
61-
return None
61+
return None
6262

63-
def run(self, q: str, overrides: dict) -> any:
63+
def run(self, q: str, overrides: dict[str, Any]) -> Any:
6464
# Not great to keep this as instance state, won't work with interleaving (e.g. if using async), but keeps the example simple
6565
self.results = None
6666

app/backend/approaches/readretrieveread.py

+15-10
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,22 @@
66
from langchain.callbacks.manager import CallbackManager, Callbacks
77
from langchain.chains import LLMChain
88
from langchain.agents import Tool, ZeroShotAgent, AgentExecutor
9-
from langchain.llms.openai import AzureOpenAI
109
from langchainadapters import HtmlCallbackHandler
1110
from text import nonewlines
1211
from lookuptool import CsvLookupTool
12+
from typing import Any
1313

14-
# Attempt to answer questions by iteratively evaluating the question to see what information is missing, and once all information
15-
# is present then formulate an answer. Each iteration consists of two parts: first use GPT to see if we need more information,
16-
# second if more data is needed use the requested "tool" to retrieve it. The last call to GPT answers the actual question.
17-
# This is inspired by the MKRL paper[1] and applied here using the implementation in Langchain.
18-
# [1] E. Karpas, et al. arXiv:2205.00445
1914
class ReadRetrieveReadApproach(Approach):
15+
"""
16+
Attempt to answer questions by iteratively evaluating the question to see what information is missing, and once all information
17+
is present then formulate an answer. Each iteration consists of two parts:
18+
1. use GPT to see if we need more information
19+
2. if more data is needed, use the requested "tool" to retrieve it.
20+
The last call to GPT answers the actual question.
21+
This is inspired by the MKRL paper[1] and applied here using the implementation in Langchain.
22+
23+
[1] E. Karpas, et al. arXiv:2205.00445
24+
"""
2025

2126
template_prefix = \
2227
"You are an intelligent assistant helping Contoso Inc employees with their healthcare plan questions and employee handbook questions. " \
@@ -45,7 +50,7 @@ def __init__(self, search_client: SearchClient, openai_deployment: str, sourcepa
4550
self.sourcepage_field = sourcepage_field
4651
self.content_field = content_field
4752

48-
def retrieve(self, q: str, overrides: dict) -> any:
53+
def retrieve(self, q: str, overrides: dict[str, Any]) -> Any:
4954
use_semantic_captions = True if overrides.get("semantic_captions") else False
5055
top = overrides.get("top") or 3
5156
exclude_category = overrides.get("exclude_category") or None
@@ -69,7 +74,7 @@ def retrieve(self, q: str, overrides: dict) -> any:
6974
content = "\n".join(self.results)
7075
return content
7176

72-
def run(self, q: str, overrides: dict) -> any:
77+
def run(self, q: str, overrides: dict[str, Any]) -> Any:
7378
# Not great to keep this as instance state, won't work with interleaving (e.g. if using async), but keeps the example simple
7479
self.results = None
7580

@@ -115,5 +120,5 @@ def __init__(self, employee_name: str, callbacks: Callbacks = None):
115120
self.func = self.employee_info
116121
self.employee_name = employee_name
117122

118-
def employee_info(self, unused: str) -> str:
119-
return self.lookup(self.employee_name)
123+
def employee_info(self, name: str) -> str:
124+
return self.lookup(name)

app/backend/approaches/retrievethenread.py

+8-4
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@
33
from azure.search.documents import SearchClient
44
from azure.search.documents.models import QueryType
55
from text import nonewlines
6+
from typing import Any
7+
68

7-
# Simple retrieve-then-read implementation, using the Cognitive Search and OpenAI APIs directly. It first retrieves
8-
# top documents from search, then constructs a prompt with them, and then uses OpenAI to generate an completion
9-
# (answer) with that prompt.
109
class RetrieveThenReadApproach(Approach):
10+
"""
11+
Simple retrieve-then-read implementation, using the Cognitive Search and OpenAI APIs directly. It first retrieves
12+
top documents from search, then constructs a prompt with them, and then uses OpenAI to generate an completion
13+
(answer) with that prompt.
14+
"""
1115

1216
template = \
1317
"You are an intelligent assistant helping Contoso Inc employees with their healthcare plan questions and employee handbook questions. " + \
@@ -45,7 +49,7 @@ def __init__(self, search_client: SearchClient, openai_deployment: str, sourcepa
4549
self.sourcepage_field = sourcepage_field
4650
self.content_field = content_field
4751

48-
def run(self, q: str, overrides: dict) -> any:
52+
def run(self, q: str, overrides: dict[str, Any]) -> Any:
4953
use_semantic_captions = True if overrides.get("semantic_captions") else False
5054
top = overrides.get("top") or 3
5155
exclude_category = overrides.get("exclude_category") or None

app/backend/langchainadapters.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
from typing import Any, Dict, List, Optional
1+
from typing import Any, Dict, List, Optional, Union
22
from langchain.callbacks.base import BaseCallbackHandler
33
from langchain.schema import AgentAction, AgentFinish, LLMResult
44

5-
def ch(text: str) -> str:
5+
def ch(text: Union[str, object]) -> str:
66
s = text if isinstance(text, str) else str(text)
77
return s.replace("<", "&lt;").replace(">", "&gt;").replace("\r", "").replace("\n", "<br>")
88

app/backend/lookuptool.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
from os import path
21
import csv
2+
from pathlib import Path
33
from langchain.agents import Tool
44
from langchain.callbacks.manager import Callbacks
5-
from typing import Optional
5+
from typing import Optional, Union
66

77
class CsvLookupTool(Tool):
88
data: dict[str, str] = {}
99

10-
def __init__(self, filename: path, key_field: str, name: str = "lookup",
10+
def __init__(self, filename: Union[str, Path], key_field: str, name: str = "lookup",
1111
description: str = "useful to look up details given an input key as opposite to searching data with an unstructured question",
1212
callbacks: Callbacks = None):
1313
super().__init__(name, self.lookup, description, callbacks=callbacks)

app/backend/requirements.txt

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
azure-identity==1.13.0b3
2-
Flask==2.2.2
1+
azure-identity==1.13.0
2+
Flask==2.2.5
33
langchain==0.0.187
44
openai==0.26.4
55
azure-search-documents==11.4.0b3

app/start.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ if [ $? -ne 0 ]; then
1717
fi
1818

1919
echo 'Creating python virtual environment "backend/backend_env"'
20-
python -m venv backend/backend_env
20+
python3 -m venv backend/backend_env
2121

2222
echo ""
2323
echo "Restoring backend python packages"

infra/core/ai/cognitiveservices.bicep

+2-2
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ resource deployment 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01
3030
model: deployment.model
3131
raiPolicyName: contains(deployment, 'raiPolicyName') ? deployment.raiPolicyName : null
3232
}
33-
sku: {
33+
sku: contains(deployment, 'sku') ? deployment.sku : {
3434
name: 'Standard'
35-
capacity: deployment.capacity
35+
capacity: 20
3636
}
3737
}]
3838

infra/main.bicep

+18-10
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@ param formRecognizerResourceGroupLocation string = location
3737

3838
param formRecognizerSkuName string = 'S0'
3939

40-
param gptDeploymentName string = 'davinci'
40+
param gptDeploymentName string = ''
4141
param gptDeploymentCapacity int = 30
4242
param gptModelName string = 'text-davinci-003'
43-
param chatGptDeploymentName string = 'chat'
43+
param chatGptDeploymentName string = ''
4444
param chatGptDeploymentCapacity int = 30
4545
param chatGptModelName string = 'gpt-35-turbo'
4646

@@ -50,6 +50,8 @@ param principalId string = ''
5050
var abbrs = loadJsonContent('abbreviations.json')
5151
var resourceToken = toLower(uniqueString(subscription().id, environmentName, location))
5252
var tags = { 'azd-env-name': environmentName }
53+
var gptDeployment = empty(gptDeploymentName) ? 'davinci' : gptDeploymentName
54+
var chatGptDeployment = empty(chatGptDeploymentName) ? 'chat' : chatGptDeploymentName
5355

5456
// Organize resources in a resource group
5557
resource resourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = {
@@ -109,8 +111,8 @@ module backend 'core/host/appservice.bicep' = {
109111
AZURE_OPENAI_SERVICE: openAi.outputs.name
110112
AZURE_SEARCH_INDEX: searchIndexName
111113
AZURE_SEARCH_SERVICE: searchService.outputs.name
112-
AZURE_OPENAI_GPT_DEPLOYMENT: gptDeploymentName
113-
AZURE_OPENAI_CHATGPT_DEPLOYMENT: chatGptDeploymentName
114+
AZURE_OPENAI_GPT_DEPLOYMENT: gptDeployment
115+
AZURE_OPENAI_CHATGPT_DEPLOYMENT: chatGptDeployment
114116
}
115117
}
116118
}
@@ -127,22 +129,28 @@ module openAi 'core/ai/cognitiveservices.bicep' = {
127129
}
128130
deployments: [
129131
{
130-
name: gptDeploymentName
132+
name: gptDeployment
131133
model: {
132134
format: 'OpenAI'
133135
name: gptModelName
134136
version: '1'
135137
}
136-
capacity: gptDeploymentCapacity
138+
sku: {
139+
name: 'Standard'
140+
capacity: gptDeploymentCapacity
141+
}
137142
}
138143
{
139-
name: chatGptDeploymentName
144+
name: chatGptDeployment
140145
model: {
141146
format: 'OpenAI'
142147
name: chatGptModelName
143148
version: '0301'
144149
}
145-
capacity: chatGptDeploymentCapacity
150+
sku: {
151+
name: 'Standard'
152+
capacity: chatGptDeploymentCapacity
153+
}
146154
}
147155
]
148156
}
@@ -313,8 +321,8 @@ output AZURE_RESOURCE_GROUP string = resourceGroup.name
313321

314322
output AZURE_OPENAI_SERVICE string = openAi.outputs.name
315323
output AZURE_OPENAI_RESOURCE_GROUP string = openAiResourceGroup.name
316-
output AZURE_OPENAI_GPT_DEPLOYMENT string = gptDeploymentName
317-
output AZURE_OPENAI_CHATGPT_DEPLOYMENT string = chatGptDeploymentName
324+
output AZURE_OPENAI_GPT_DEPLOYMENT string = gptDeployment
325+
output AZURE_OPENAI_CHATGPT_DEPLOYMENT string = chatGptDeployment
318326

319327
output AZURE_FORMRECOGNIZER_SERVICE string = formRecognizer.outputs.name
320328
output AZURE_FORMRECOGNIZER_RESOURCE_GROUP string = formRecognizerResourceGroup.name

infra/main.parameters.json

+6
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@
4343
},
4444
"storageResourceGroupName": {
4545
"value": "${AZURE_STORAGE_RESOURCE_GROUP}"
46+
},
47+
"chatGptDeploymentName": {
48+
"value": "${AZURE_OPENAI_CHATGPT_DEPLOYMENT}"
49+
},
50+
"gptDeploymentName": {
51+
"value": "${AZURE_OPENAI_GPT_DEPLOYMENT}"
4652
}
4753
}
4854
}

0 commit comments

Comments
 (0)