Skip to content

Commit

Permalink
initial commit for Azure RAG cookbook (openai#1272)
Browse files Browse the repository at this point in the history
Co-authored-by: juston <96567547+justonf@users.noreply.github.com>
  • Loading branch information
maxreid-openai and justonf authored Jul 25, 2024
1 parent f6ea13e commit 5f55266
Show file tree
Hide file tree
Showing 64 changed files with 13,182 additions and 9 deletions.
1 change: 1 addition & 0 deletions examples/chatgpt/rag-quickstart/azure/.funcignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

50 changes: 50 additions & 0 deletions examples/chatgpt/rag-quickstart/azure/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
bin
obj
csx
.vs
edge
Publish

*.user
*.suo
*.cscfg
*.Cache
project.lock.json

/packages
/TestResults

/tools/NuGet.exe
/App_Data
/secrets
/data
.secrets
appsettings.json
local.settings.json

node_modules
dist
vector_database_wikipedia_articles_embedded

# Local python packages
.python_packages/

# Python Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# Azurite artifacts
__blobstorage__
__queuestorage__
__azurite_db*__.json
vector_database_wikipedia_articles_embedded
5 changes: 5 additions & 0 deletions examples/chatgpt/rag-quickstart/azure/.vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"recommendations": [
"ms-azuretools.vscode-azurefunctions"
]
}

Large diffs are not rendered by default.

153 changes: 153 additions & 0 deletions examples/chatgpt/rag-quickstart/azure/function_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import azure.functions as func
import json
import logging
from azure.search.documents import SearchClient
from azure.search.documents.indexes import SearchIndexClient
from azure.core.credentials import AzureKeyCredential
from openai import OpenAI
import os
from azure.search.documents.models import (
VectorizedQuery
)

# Initialize the Azure Function App
app = func.FunctionApp()

def generate_embeddings(text):
# Check if text is provided
if not text:
logging.error("No text provided in the query string.")
return func.HttpResponse(
"Please provide text in the query string.",
status_code=400
)

try:
# Initialize OpenAI client
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
logging.info("OpenAI client initialized successfully.")

# Generate embeddings using OpenAI API
response = client.embeddings.create(
input=text,
model=os.getenv("EMBEDDINGS_MODEL")
)
logging.info("Embeddings created successfully.")

# Extract the embedding from the response
embedding = response.data[0].embedding
logging.debug(f"Generated embedding: {embedding}")

return embedding
except Exception as e:
logging.error(f"Error generating embeddings: {str(e)}")
return func.HttpResponse(
f"Error generating embeddings: {str(e)}",
status_code=500
)


@app.route(route="vector_similarity_search", auth_level=func.AuthLevel.ANONYMOUS)
def vector_similarity_search(req: func.HttpRequest) -> func.HttpResponse:
logging.info("Received request for vector similarity search.")
try:
# Parse the request body as JSON
req_body = req.get_json()
logging.info("Request body parsed successfully.")
except ValueError:
logging.error("Invalid JSON in request body.")
return func.HttpResponse(
"Invalid JSON in request body.",
status_code=400
)

# Extract parameters from the request body
search_service_endpoint = req_body.get('search_service_endpoint')
index_name = req_body.get('index_name')
query = req_body.get('query')
k_nearest_neighbors = req_body.get('k_nearest_neighbors')
search_column = req_body.get('search_column')
use_hybrid_query = req_body.get('use_hybrid_query')

logging.info(f"Parsed request parameters: search_service_endpoint={search_service_endpoint}, index_name={index_name}, query={query}, k_nearest_neighbors={k_nearest_neighbors}, search_column={search_column}, use_hybrid_query={use_hybrid_query}")

# Generate embeddings for the query
embeddings = generate_embeddings(query)
logging.info(f"Generated embeddings: {embeddings}")

# Check for required parameters
if not (search_service_endpoint and index_name and query):
logging.error("Missing required parameters in request body.")
return func.HttpResponse(
"Please provide search_service_endpoint, index_name, and query in the request body.",
status_code=400
)
try:
# Create a vectorized query
vector_query = VectorizedQuery(vector=embeddings, k_nearest_neighbors=float(k_nearest_neighbors), fields=search_column)
logging.info("Vector query generated successfully.")
except Exception as e:
logging.error(f"Error generating vector query: {str(e)}")
return func.HttpResponse(
f"Error generating vector query: {str(e)}",
status_code=500
)

try:
# Initialize the search client
search_client = SearchClient(
endpoint=search_service_endpoint,
index_name=index_name,
credential=AzureKeyCredential(os.getenv("SEARCH_SERVICE_API_KEY"))
)
logging.info("Search client created successfully.")

# Initialize the index client and get the index schema
index_client = SearchIndexClient(endpoint=search_service_endpoint, credential=AzureKeyCredential(os.getenv("SEARCH_SERVICE_API_KEY")))
index_schema = index_client.get_index(index_name)
for field in index_schema.fields:
logging.info(f"Field: {field.name}, Type: {field.type}")
# Filter out non-vector fields
non_vector_fields = [field.name for field in index_schema.fields if field.type not in ["Edm.ComplexType", "Collection(Edm.ComplexType)","Edm.Vector","Collection(Edm.Single)"]]

logging.info(f"Non-vector fields in the index: {non_vector_fields}")
except Exception as e:
logging.error(f"Error creating search client: {str(e)}")
return func.HttpResponse(
f"Error creating search client: {str(e)}",
status_code=500
)

# Determine if hybrid query should be used
search_text = query if use_hybrid_query else None

try:
# Perform the search
results = search_client.search(
search_text=search_text,
vector_queries=[vector_query],
select=non_vector_fields,
top=3
)
logging.info("Search performed successfully.")
except Exception as e:
logging.error(f"Error performing search: {str(e)}")
return func.HttpResponse(
f"Error performing search: {str(e)}",
status_code=500
)

try:
# Extract relevant data from results and put it into a list of dictionaries
response_data = [result for result in results]
response_data = json.dumps(response_data)
logging.info("Search results processed successfully.")
except Exception as e:
logging.error(f"Error processing search results: {str(e)}")
return func.HttpResponse(
f"Error processing search results: {str(e)}",
status_code=500
)

logging.info("Returning search results.")
return func.HttpResponse(response_data, mimetype="application/json")
15 changes: 15 additions & 0 deletions examples/chatgpt/rag-quickstart/azure/host.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"excludedTypes": "Request"
}
}
},
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[4.*, 5.0.0)"
}
}
17 changes: 17 additions & 0 deletions examples/chatgpt/rag-quickstart/azure/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Do not include azure-functions-worker in this file
# The Python Worker is managed by the Azure Functions platform
# Manually managing azure-functions-worker may cause unexpected issues

azure-functions
azure-search-documents
azure-identity
openai
azure-mgmt-search
pandas
azure-mgmt-resource
azure-mgmt-storage
azure-mgmt-web
python-dotenv
pyperclip
PyPDF2
tiktoken
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"scriptFile": "__init__.py",
"bindings": [
{
"authLevel": "Anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"post"
]
},
{
"type": "http",
"direction": "out",
"name": "$return"
}
]
}
34 changes: 34 additions & 0 deletions examples/data/oai_docs/authentication.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@

# Action authentication

Actions offer different authentication schemas to accommodate various use cases. To specify the authentication schema for your action, use the GPT editor and select "None", "API Key", or "OAuth".

By default, the authentication method for all actions is set to "None", but you can change this and allow different actions to have different authentication methods.

## No authentication

We support flows without authentication for applications where users can send requests directly to your API without needing an API key or signing in with OAuth.

Consider using no authentication for initial user interactions as you might experience a user drop off if they are forced to sign into an application. You can create a "signed out" experience and then move users to a "signed in" experience by enabling a separate action.

## API key authentication

Just like how a user might already be using your API, we allow API key authentication through the GPT editor UI. We encrypt the secret key when we store it in our database to keep your API key secure.

This approach is useful if you have an API that takes slightly more consequential actions than the no authentication flow but does not require an individual user to sign in. Adding API key authentication can protect your API and give you more fine-grained access controls along with visibility into where requests are coming from.

## OAuth

Actions allow OAuth sign in for each user. This is the best way to provide personalized experiences and make the most powerful actions available to users. A simple example of the OAuth flow with actions will look like the following:

- To start, select "Authentication" in the GPT editor UI, and select "OAuth".
- You will be prompted to enter the OAuth client ID, client secret, authorization URL, token URL, and scope.
- The client ID and secret can be simple text strings but should [follow OAuth best practices](https://www.oauth.com/oauth2-servers/client-registration/client-id-secret/).
- We store an encrypted version of the client secret, while the client ID is available to end users.
- OAuth requests will include the following information: `request={'grant_type': 'authorization_code', 'client_id': 'YOUR_CLIENT_ID', 'client_secret': 'YOUR_CLIENT_SECRET', 'code': 'abc123', 'redirect_uri': 'https://chatgpt.com/aip/g-some_gpt_id/oauth/callback'}`
- In order for someone to use an action with OAuth, they will need to send a message that invokes the action and then the user will be presented with a "Sign in to [domain]" button in the ChatGPT UI.
- The `authorization_url` endpoint should return a response that looks like:
`{ "access_token": "example_token", "token_type": "bearer", "refresh_token": "example_token", "expires_in": 59 }`
- During the user sign in process, ChatGPT makes a request to your `authorization_url` using the specified `authorization_content_type`, we expect to get back an access token and optionally a [refresh token](https://auth0.com/learn/refresh-tokens) which we use to periodically fetch a new access token.
- Each time a user makes a request to the action, the user’s token will be passed in the Authorization header: (“Authorization”: “[Bearer/Basic] [user’s token]”).
- We require that OAuth applications make use of the [state parameter](https://auth0.com/docs/secure/attack-protection/state-parameters#set-and-compare-state-parameter-values) for security reasons.
Loading

0 comments on commit 5f55266

Please sign in to comment.