-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature: Store chat history in Cosmos DB (#2063)
* Add a Cosmos DB resource for chat history * Added Cosmos DB chat history feature to the backend * Added Cosmos DB chat history feature to the frontend * fix typo * Reconfigure the additional API endpoints in a separate blueprint * Refactor CosmosDB integration for chat history feature Use the timestamp property instead of _ts * Update docs * Change cast to type annotation, add initial test * Use authenticated decorators properly * Add more unit tests for CosmosDB * Remove unreachable code path, change 200 to 204 for delete, more tests * Address mypy issues * Update auth test with a session ID * Delete volatile property from snapshot --------- Co-authored-by: Pamela Fox <[email protected]>
- Loading branch information
Showing
30 changed files
with
984 additions
and
35 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
import os | ||
import time | ||
from typing import Any, Dict, Union | ||
|
||
from azure.cosmos.aio import ContainerProxy, CosmosClient | ||
from azure.identity.aio import AzureDeveloperCliCredential, ManagedIdentityCredential | ||
from quart import Blueprint, current_app, jsonify, request | ||
|
||
from config import ( | ||
CONFIG_CHAT_HISTORY_COSMOS_ENABLED, | ||
CONFIG_COSMOS_HISTORY_CLIENT, | ||
CONFIG_COSMOS_HISTORY_CONTAINER, | ||
CONFIG_CREDENTIAL, | ||
) | ||
from decorators import authenticated | ||
from error import error_response | ||
|
||
chat_history_cosmosdb_bp = Blueprint("chat_history_cosmos", __name__, static_folder="static") | ||
|
||
|
||
@chat_history_cosmosdb_bp.post("/chat_history") | ||
@authenticated | ||
async def post_chat_history(auth_claims: Dict[str, Any]): | ||
if not current_app.config[CONFIG_CHAT_HISTORY_COSMOS_ENABLED]: | ||
return jsonify({"error": "Chat history not enabled"}), 400 | ||
|
||
container: ContainerProxy = current_app.config[CONFIG_COSMOS_HISTORY_CONTAINER] | ||
if not container: | ||
return jsonify({"error": "Chat history not enabled"}), 400 | ||
|
||
entra_oid = auth_claims.get("oid") | ||
if not entra_oid: | ||
return jsonify({"error": "User OID not found"}), 401 | ||
|
||
try: | ||
request_json = await request.get_json() | ||
id = request_json.get("id") | ||
answers = request_json.get("answers") | ||
title = answers[0][0][:50] + "..." if len(answers[0][0]) > 50 else answers[0][0] | ||
timestamp = int(time.time() * 1000) | ||
|
||
await container.upsert_item( | ||
{"id": id, "entra_oid": entra_oid, "title": title, "answers": answers, "timestamp": timestamp} | ||
) | ||
|
||
return jsonify({}), 201 | ||
except Exception as error: | ||
return error_response(error, "/chat_history") | ||
|
||
|
||
@chat_history_cosmosdb_bp.post("/chat_history/items") | ||
@authenticated | ||
async def get_chat_history(auth_claims: Dict[str, Any]): | ||
if not current_app.config[CONFIG_CHAT_HISTORY_COSMOS_ENABLED]: | ||
return jsonify({"error": "Chat history not enabled"}), 400 | ||
|
||
container: ContainerProxy = current_app.config[CONFIG_COSMOS_HISTORY_CONTAINER] | ||
if not container: | ||
return jsonify({"error": "Chat history not enabled"}), 400 | ||
|
||
entra_oid = auth_claims.get("oid") | ||
if not entra_oid: | ||
return jsonify({"error": "User OID not found"}), 401 | ||
|
||
try: | ||
request_json = await request.get_json() | ||
count = request_json.get("count", 20) | ||
continuation_token = request_json.get("continuation_token") | ||
|
||
res = container.query_items( | ||
query="SELECT c.id, c.entra_oid, c.title, c.timestamp FROM c WHERE c.entra_oid = @entra_oid ORDER BY c.timestamp DESC", | ||
parameters=[dict(name="@entra_oid", value=entra_oid)], | ||
max_item_count=count, | ||
) | ||
|
||
# set the continuation token for the next page | ||
pager = res.by_page(continuation_token) | ||
|
||
# Get the first page, and the continuation token | ||
try: | ||
page = await pager.__anext__() | ||
continuation_token = pager.continuation_token # type: ignore | ||
|
||
items = [] | ||
async for item in page: | ||
items.append( | ||
{ | ||
"id": item.get("id"), | ||
"entra_oid": item.get("entra_oid"), | ||
"title": item.get("title", "untitled"), | ||
"timestamp": item.get("timestamp"), | ||
} | ||
) | ||
|
||
# If there are no more pages, StopAsyncIteration is raised | ||
except StopAsyncIteration: | ||
items = [] | ||
continuation_token = None | ||
|
||
return jsonify({"items": items, "continuation_token": continuation_token}), 200 | ||
|
||
except Exception as error: | ||
return error_response(error, "/chat_history/items") | ||
|
||
|
||
@chat_history_cosmosdb_bp.get("/chat_history/items/<item_id>") | ||
@authenticated | ||
async def get_chat_history_session(auth_claims: Dict[str, Any], item_id: str): | ||
if not current_app.config[CONFIG_CHAT_HISTORY_COSMOS_ENABLED]: | ||
return jsonify({"error": "Chat history not enabled"}), 400 | ||
|
||
container: ContainerProxy = current_app.config[CONFIG_COSMOS_HISTORY_CONTAINER] | ||
if not container: | ||
return jsonify({"error": "Chat history not enabled"}), 400 | ||
|
||
entra_oid = auth_claims.get("oid") | ||
if not entra_oid: | ||
return jsonify({"error": "User OID not found"}), 401 | ||
|
||
try: | ||
res = await container.read_item(item=item_id, partition_key=entra_oid) | ||
return ( | ||
jsonify( | ||
{ | ||
"id": res.get("id"), | ||
"entra_oid": res.get("entra_oid"), | ||
"title": res.get("title", "untitled"), | ||
"timestamp": res.get("timestamp"), | ||
"answers": res.get("answers", []), | ||
} | ||
), | ||
200, | ||
) | ||
except Exception as error: | ||
return error_response(error, f"/chat_history/items/{item_id}") | ||
|
||
|
||
@chat_history_cosmosdb_bp.delete("/chat_history/items/<item_id>") | ||
@authenticated | ||
async def delete_chat_history_session(auth_claims: Dict[str, Any], item_id: str): | ||
if not current_app.config[CONFIG_CHAT_HISTORY_COSMOS_ENABLED]: | ||
return jsonify({"error": "Chat history not enabled"}), 400 | ||
|
||
container: ContainerProxy = current_app.config[CONFIG_COSMOS_HISTORY_CONTAINER] | ||
if not container: | ||
return jsonify({"error": "Chat history not enabled"}), 400 | ||
|
||
entra_oid = auth_claims.get("oid") | ||
if not entra_oid: | ||
return jsonify({"error": "User OID not found"}), 401 | ||
|
||
try: | ||
await container.delete_item(item=item_id, partition_key=entra_oid) | ||
return jsonify({}), 204 | ||
except Exception as error: | ||
return error_response(error, f"/chat_history/items/{item_id}") | ||
|
||
|
||
@chat_history_cosmosdb_bp.before_app_serving | ||
async def setup_clients(): | ||
USE_CHAT_HISTORY_COSMOS = os.getenv("USE_CHAT_HISTORY_COSMOS", "").lower() == "true" | ||
AZURE_COSMOSDB_ACCOUNT = os.getenv("AZURE_COSMOSDB_ACCOUNT") | ||
AZURE_CHAT_HISTORY_DATABASE = os.getenv("AZURE_CHAT_HISTORY_DATABASE") | ||
AZURE_CHAT_HISTORY_CONTAINER = os.getenv("AZURE_CHAT_HISTORY_CONTAINER") | ||
|
||
azure_credential: Union[AzureDeveloperCliCredential, ManagedIdentityCredential] = current_app.config[ | ||
CONFIG_CREDENTIAL | ||
] | ||
|
||
if USE_CHAT_HISTORY_COSMOS: | ||
current_app.logger.info("USE_CHAT_HISTORY_COSMOS is true, setting up CosmosDB client") | ||
if not AZURE_COSMOSDB_ACCOUNT: | ||
raise ValueError("AZURE_COSMOSDB_ACCOUNT must be set when USE_CHAT_HISTORY_COSMOS is true") | ||
if not AZURE_CHAT_HISTORY_DATABASE: | ||
raise ValueError("AZURE_CHAT_HISTORY_DATABASE must be set when USE_CHAT_HISTORY_COSMOS is true") | ||
if not AZURE_CHAT_HISTORY_CONTAINER: | ||
raise ValueError("AZURE_CHAT_HISTORY_CONTAINER must be set when USE_CHAT_HISTORY_COSMOS is true") | ||
cosmos_client = CosmosClient( | ||
url=f"https://{AZURE_COSMOSDB_ACCOUNT}.documents.azure.com:443/", credential=azure_credential | ||
) | ||
cosmos_db = cosmos_client.get_database_client(AZURE_CHAT_HISTORY_DATABASE) | ||
cosmos_container = cosmos_db.get_container_client(AZURE_CHAT_HISTORY_CONTAINER) | ||
|
||
current_app.config[CONFIG_COSMOS_HISTORY_CLIENT] = cosmos_client | ||
current_app.config[CONFIG_COSMOS_HISTORY_CONTAINER] = cosmos_container | ||
|
||
|
||
@chat_history_cosmosdb_bp.after_app_serving | ||
async def close_clients(): | ||
if current_app.config.get(CONFIG_COSMOS_HISTORY_CLIENT): | ||
cosmos_client: CosmosClient = current_app.config[CONFIG_COSMOS_HISTORY_CLIENT] | ||
await cosmos_client.close() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.