Skip to content

Commit 6846c4d

Browse files
committed
Added Cosmos DB chat history feature to the backend
1 parent bbc74de commit 6846c4d

File tree

5 files changed

+169
-3
lines changed

5 files changed

+169
-3
lines changed

app/backend/app.py

Lines changed: 156 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
SpeechSynthesizer,
1717
)
1818
from azure.core.exceptions import ResourceNotFoundError
19+
from azure.cosmos.aio import ContainerProxy, CosmosClient
1920
from azure.identity.aio import (
2021
AzureDeveloperCliCredential,
2122
ManagedIdentityCredential,
@@ -60,7 +61,9 @@
6061
CONFIG_BLOB_CONTAINER_CLIENT,
6162
CONFIG_CHAT_APPROACH,
6263
CONFIG_CHAT_HISTORY_BROWSER_ENABLED,
64+
CONFIG_CHAT_HISTORY_COSMOS_ENABLED,
6365
CONFIG_CHAT_VISION_APPROACH,
66+
CONFIG_COSMOS_HISTORY_CONTAINER,
6467
CONFIG_CREDENTIAL,
6568
CONFIG_GPT4V_DEPLOYED,
6669
CONFIG_INGESTER,
@@ -224,7 +227,10 @@ async def chat(auth_claims: Dict[str, Any]):
224227
# else creates a new session_id depending on the chat history options enabled.
225228
session_state = request_json.get("session_state")
226229
if session_state is None:
227-
session_state = create_session_id(current_app.config[CONFIG_CHAT_HISTORY_BROWSER_ENABLED])
230+
session_state = create_session_id(
231+
current_app.config[CONFIG_CHAT_HISTORY_COSMOS_ENABLED],
232+
current_app.config[CONFIG_CHAT_HISTORY_BROWSER_ENABLED],
233+
)
228234
result = await approach.run(
229235
request_json["messages"],
230236
context=context,
@@ -255,7 +261,10 @@ async def chat_stream(auth_claims: Dict[str, Any]):
255261
# else creates a new session_id depending on the chat history options enabled.
256262
session_state = request_json.get("session_state")
257263
if session_state is None:
258-
session_state = create_session_id(current_app.config[CONFIG_CHAT_HISTORY_BROWSER_ENABLED])
264+
session_state = create_session_id(
265+
current_app.config[CONFIG_CHAT_HISTORY_COSMOS_ENABLED],
266+
current_app.config[CONFIG_CHAT_HISTORY_BROWSER_ENABLED],
267+
)
259268
result = await approach.run_stream(
260269
request_json["messages"],
261270
context=context,
@@ -289,6 +298,7 @@ def config():
289298
"showSpeechOutputBrowser": current_app.config[CONFIG_SPEECH_OUTPUT_BROWSER_ENABLED],
290299
"showSpeechOutputAzure": current_app.config[CONFIG_SPEECH_OUTPUT_AZURE_ENABLED],
291300
"showChatHistoryBrowser": current_app.config[CONFIG_CHAT_HISTORY_BROWSER_ENABLED],
301+
"showChatHistoryCosmos": current_app.config[CONFIG_CHAT_HISTORY_COSMOS_ENABLED],
292302
}
293303
)
294304

@@ -397,6 +407,129 @@ async def list_uploaded(auth_claims: dict[str, Any]):
397407
return jsonify(files), 200
398408

399409

410+
@bp.post("/chat_history")
411+
@authenticated
412+
async def post_chat_history(auth_claims: Dict[str, Any]):
413+
if not current_app.config[CONFIG_CHAT_HISTORY_COSMOS_ENABLED]:
414+
return jsonify({"error": "Chat history not enabled"}), 405
415+
416+
container = cast(ContainerProxy, current_app.config[CONFIG_COSMOS_HISTORY_CONTAINER])
417+
if not container:
418+
return jsonify({"error": "Chat history not enabled"}), 405
419+
420+
entra_oid = auth_claims.get("oid")
421+
if not entra_oid:
422+
return jsonify({"error": "User OID not found"}), 401
423+
424+
try:
425+
request_json = await request.get_json()
426+
id = request_json.get("id")
427+
answers = request_json.get("answers")
428+
title = answers[0][0][:50] + "..." if len(answers[0][0]) > 50 else answers[0][0]
429+
430+
await container.upsert_item({"id": id, "entra_oid": entra_oid, "title": title, "answers": answers})
431+
432+
return jsonify({}), 201
433+
except Exception as error:
434+
return error_response(error, "/chat_history")
435+
436+
437+
@bp.post("/chat_history/items")
438+
@authenticated
439+
async def get_chat_history(auth_claims: Dict[str, Any]):
440+
if not current_app.config[CONFIG_CHAT_HISTORY_COSMOS_ENABLED]:
441+
return jsonify({"error": "Chat history not enabled"}), 405
442+
443+
container = cast(ContainerProxy, current_app.config[CONFIG_COSMOS_HISTORY_CONTAINER])
444+
if not container:
445+
return jsonify({"error": "Chat history not enabled"}), 405
446+
447+
entra_oid = auth_claims.get("oid")
448+
if not entra_oid:
449+
return jsonify({"error": "User OID not found"}), 401
450+
451+
try:
452+
request_json = await request.get_json()
453+
count = request_json.get("count", 20)
454+
continuation_token = request_json.get("continuation_token")
455+
456+
res = container.query_items(
457+
query="SELECT c.id, c.entra_oid, c.title, c._ts FROM c WHERE c.entra_oid = @entra_oid ORDER BY c._ts DESC",
458+
parameters=[dict(name="@entra_oid", value=entra_oid)],
459+
max_item_count=count,
460+
)
461+
462+
# set the continuation token for the next page
463+
pager = res.by_page(continuation_token)
464+
465+
# Get the first page, and the continuation token
466+
try:
467+
page = await pager.__anext__()
468+
continuation_token = pager.continuation_token # type: ignore
469+
470+
items = []
471+
async for item in page:
472+
items.append(item)
473+
474+
# If there are no page, StopAsyncIteration is raised
475+
except StopAsyncIteration:
476+
items = []
477+
continuation_token = None
478+
479+
return jsonify({"items": items, "continuation_token": continuation_token}), 200
480+
481+
except Exception as error:
482+
return error_response(error, "/chat_history/items")
483+
484+
485+
@bp.get("/chat_history/items/<path>")
486+
@authenticated_path
487+
async def get_chat_history_session(path: str, auth_claims: Dict[str, Any]):
488+
if not current_app.config[CONFIG_CHAT_HISTORY_COSMOS_ENABLED]:
489+
return jsonify({"error": "Chat history not enabled"}), 405
490+
491+
container = cast(ContainerProxy, current_app.config[CONFIG_COSMOS_HISTORY_CONTAINER])
492+
if not container:
493+
return jsonify({"error": "Chat history not enabled"}), 405
494+
495+
if not path:
496+
return jsonify({"error": "Invalid path"}), 400
497+
498+
entra_oid = auth_claims.get("oid")
499+
if not entra_oid:
500+
return jsonify({"error": "User OID not found"}), 401
501+
502+
try:
503+
res = await container.read_item(item=path, partition_key=entra_oid)
504+
return jsonify(res), 200
505+
except Exception as error:
506+
return error_response(error, f"/chat_history/items/{path}")
507+
508+
509+
@bp.delete("/chat_history/items/<path>")
510+
@authenticated_path
511+
async def delete_chat_history_session(path: str, auth_claims: Dict[str, Any]):
512+
if not current_app.config[CONFIG_CHAT_HISTORY_COSMOS_ENABLED]:
513+
return jsonify({"error": "Chat history not enabled"}), 405
514+
515+
container = cast(ContainerProxy, current_app.config[CONFIG_COSMOS_HISTORY_CONTAINER])
516+
if not container:
517+
return jsonify({"error": "Chat history not enabled"}), 405
518+
519+
if not path:
520+
return jsonify({"error": "Invalid path"}), 400
521+
522+
entra_oid = auth_claims.get("oid")
523+
if not entra_oid:
524+
return jsonify({"error": "User OID not found"}), 401
525+
526+
try:
527+
await container.delete_item(item=path, partition_key=entra_oid)
528+
return jsonify({}), 200
529+
except Exception as error:
530+
return error_response(error, f"/chat_history/items/{path}")
531+
532+
400533
@bp.before_app_serving
401534
async def setup_clients():
402535
# Replace these with your own values, either in environment variables or directly here
@@ -452,7 +585,12 @@ async def setup_clients():
452585
USE_SPEECH_INPUT_BROWSER = os.getenv("USE_SPEECH_INPUT_BROWSER", "").lower() == "true"
453586
USE_SPEECH_OUTPUT_BROWSER = os.getenv("USE_SPEECH_OUTPUT_BROWSER", "").lower() == "true"
454587
USE_SPEECH_OUTPUT_AZURE = os.getenv("USE_SPEECH_OUTPUT_AZURE", "").lower() == "true"
588+
455589
USE_CHAT_HISTORY_BROWSER = os.getenv("USE_CHAT_HISTORY_BROWSER", "").lower() == "true"
590+
USE_CHAT_HISTORY_COSMOS = os.getenv("USE_CHAT_HISTORY_COSMOS", "").lower() == "true"
591+
AZURE_COSMOSDB_ACCOUNT = os.getenv("AZURE_COSMOSDB_ACCOUNT")
592+
AZURE_CHAT_HISTORY_DATABASE = os.getenv("AZURE_CHAT_HISTORY_DATABASE")
593+
AZURE_CHAT_HISTORY_CONTAINER = os.getenv("AZURE_CHAT_HISTORY_CONTAINER")
456594

457595
# WEBSITE_HOSTNAME is always set by App Service, RUNNING_IN_PRODUCTION is set in main.bicep
458596
RUNNING_ON_AZURE = os.getenv("WEBSITE_HOSTNAME") is not None or os.getenv("RUNNING_IN_PRODUCTION") is not None
@@ -556,6 +694,21 @@ async def setup_clients():
556694
)
557695
current_app.config[CONFIG_INGESTER] = ingester
558696

697+
if USE_CHAT_HISTORY_COSMOS:
698+
current_app.logger.info("USE_CHAT_HISTORY_COSMOS is true, setting up CosmosDB client")
699+
if not AZURE_COSMOSDB_ACCOUNT:
700+
raise ValueError("AZURE_COSMOSDB_ACCOUNT must be set when USE_CHAT_HISTORY_COSMOS is true")
701+
if not AZURE_CHAT_HISTORY_DATABASE:
702+
raise ValueError("AZURE_CHAT_HISTORY_DATABASE must be set when USE_CHAT_HISTORY_COSMOS is true")
703+
if not AZURE_CHAT_HISTORY_CONTAINER:
704+
raise ValueError("AZURE_CHAT_HISTORY_CONTAINER must be set when USE_CHAT_HISTORY_COSMOS is true")
705+
cosmos_client = CosmosClient(
706+
url=f"https://{AZURE_COSMOSDB_ACCOUNT}.documents.azure.com:443/", credential=azure_credential
707+
)
708+
cosmos_db = cosmos_client.get_database_client(AZURE_CHAT_HISTORY_DATABASE)
709+
cosmos_container = cosmos_db.get_container_client(AZURE_CHAT_HISTORY_CONTAINER)
710+
current_app.config[CONFIG_COSMOS_HISTORY_CONTAINER] = cosmos_container
711+
559712
# Used by the OpenAI SDK
560713
openai_client: AsyncOpenAI
561714

@@ -624,6 +777,7 @@ async def setup_clients():
624777
current_app.config[CONFIG_SPEECH_OUTPUT_BROWSER_ENABLED] = USE_SPEECH_OUTPUT_BROWSER
625778
current_app.config[CONFIG_SPEECH_OUTPUT_AZURE_ENABLED] = USE_SPEECH_OUTPUT_AZURE
626779
current_app.config[CONFIG_CHAT_HISTORY_BROWSER_ENABLED] = USE_CHAT_HISTORY_BROWSER
780+
current_app.config[CONFIG_CHAT_HISTORY_COSMOS_ENABLED] = USE_CHAT_HISTORY_COSMOS
627781

628782
# Various approaches to integrate GPT and external knowledge, most applications will use a single one of these patterns
629783
# or some derivative, here we include several for exploration purposes

app/backend/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,5 @@
2323
CONFIG_SPEECH_SERVICE_TOKEN = "speech_service_token"
2424
CONFIG_SPEECH_SERVICE_VOICE = "speech_service_voice"
2525
CONFIG_CHAT_HISTORY_BROWSER_ENABLED = "chat_history_browser_enabled"
26+
CONFIG_CHAT_HISTORY_COSMOS_ENABLED = "chat_history_cosmos_enabled"
27+
CONFIG_COSMOS_HISTORY_CONTAINER = "cosmos_history_container"

app/backend/core/sessionhelper.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
from typing import Union
33

44

5-
def create_session_id(config_chat_history_browser_enabled: bool) -> Union[str, None]:
5+
def create_session_id(
6+
config_chat_history_cosmos_enabled: bool, config_chat_history_browser_enabled: bool
7+
) -> Union[str, None]:
8+
if config_chat_history_cosmos_enabled:
9+
return str(uuid.uuid4())
610
if config_chat_history_browser_enabled:
711
return str(uuid.uuid4())
812
return None

app/backend/requirements.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ tiktoken
77
tenacity
88
azure-ai-documentintelligence
99
azure-cognitiveservices-speech
10+
azure-cosmos
1011
azure-search-documents==11.6.0b6
1112
azure-storage-blob
1213
azure-storage-file-datalake

app/backend/requirements.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ azure-core==1.30.2
3434
# via
3535
# azure-ai-documentintelligence
3636
# azure-core-tracing-opentelemetry
37+
# azure-cosmos
3738
# azure-identity
3839
# azure-monitor-opentelemetry
3940
# azure-monitor-opentelemetry-exporter
@@ -44,6 +45,8 @@ azure-core==1.30.2
4445
# msrest
4546
azure-core-tracing-opentelemetry==1.0.0b11
4647
# via azure-monitor-opentelemetry
48+
azure-cosmos==4.7.0
49+
# via -r requirements.in
4750
azure-identity==1.17.1
4851
# via
4952
# -r requirements.in
@@ -402,7 +405,9 @@ typing-extensions==4.12.2
402405
# via
403406
# azure-ai-documentintelligence
404407
# azure-core
408+
# azure-cosmos
405409
# azure-identity
410+
# azure-search-documents
406411
# azure-storage-blob
407412
# azure-storage-file-datalake
408413
# openai

0 commit comments

Comments
 (0)