|
16 | 16 | SpeechSynthesizer,
|
17 | 17 | )
|
18 | 18 | from azure.core.exceptions import ResourceNotFoundError
|
| 19 | +from azure.cosmos.aio import ContainerProxy, CosmosClient |
19 | 20 | from azure.identity.aio import (
|
20 | 21 | AzureDeveloperCliCredential,
|
21 | 22 | ManagedIdentityCredential,
|
|
60 | 61 | CONFIG_BLOB_CONTAINER_CLIENT,
|
61 | 62 | CONFIG_CHAT_APPROACH,
|
62 | 63 | CONFIG_CHAT_HISTORY_BROWSER_ENABLED,
|
| 64 | + CONFIG_CHAT_HISTORY_COSMOS_ENABLED, |
63 | 65 | CONFIG_CHAT_VISION_APPROACH,
|
| 66 | + CONFIG_COSMOS_HISTORY_CONTAINER, |
64 | 67 | CONFIG_CREDENTIAL,
|
65 | 68 | CONFIG_GPT4V_DEPLOYED,
|
66 | 69 | CONFIG_INGESTER,
|
@@ -224,7 +227,10 @@ async def chat(auth_claims: Dict[str, Any]):
|
224 | 227 | # else creates a new session_id depending on the chat history options enabled.
|
225 | 228 | session_state = request_json.get("session_state")
|
226 | 229 | 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 | + ) |
228 | 234 | result = await approach.run(
|
229 | 235 | request_json["messages"],
|
230 | 236 | context=context,
|
@@ -255,7 +261,10 @@ async def chat_stream(auth_claims: Dict[str, Any]):
|
255 | 261 | # else creates a new session_id depending on the chat history options enabled.
|
256 | 262 | session_state = request_json.get("session_state")
|
257 | 263 | 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 | + ) |
259 | 268 | result = await approach.run_stream(
|
260 | 269 | request_json["messages"],
|
261 | 270 | context=context,
|
@@ -289,6 +298,7 @@ def config():
|
289 | 298 | "showSpeechOutputBrowser": current_app.config[CONFIG_SPEECH_OUTPUT_BROWSER_ENABLED],
|
290 | 299 | "showSpeechOutputAzure": current_app.config[CONFIG_SPEECH_OUTPUT_AZURE_ENABLED],
|
291 | 300 | "showChatHistoryBrowser": current_app.config[CONFIG_CHAT_HISTORY_BROWSER_ENABLED],
|
| 301 | + "showChatHistoryCosmos": current_app.config[CONFIG_CHAT_HISTORY_COSMOS_ENABLED], |
292 | 302 | }
|
293 | 303 | )
|
294 | 304 |
|
@@ -397,6 +407,129 @@ async def list_uploaded(auth_claims: dict[str, Any]):
|
397 | 407 | return jsonify(files), 200
|
398 | 408 |
|
399 | 409 |
|
| 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 | + |
400 | 533 | @bp.before_app_serving
|
401 | 534 | async def setup_clients():
|
402 | 535 | # Replace these with your own values, either in environment variables or directly here
|
@@ -452,7 +585,12 @@ async def setup_clients():
|
452 | 585 | USE_SPEECH_INPUT_BROWSER = os.getenv("USE_SPEECH_INPUT_BROWSER", "").lower() == "true"
|
453 | 586 | USE_SPEECH_OUTPUT_BROWSER = os.getenv("USE_SPEECH_OUTPUT_BROWSER", "").lower() == "true"
|
454 | 587 | USE_SPEECH_OUTPUT_AZURE = os.getenv("USE_SPEECH_OUTPUT_AZURE", "").lower() == "true"
|
| 588 | + |
455 | 589 | 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") |
456 | 594 |
|
457 | 595 | # WEBSITE_HOSTNAME is always set by App Service, RUNNING_IN_PRODUCTION is set in main.bicep
|
458 | 596 | 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():
|
556 | 694 | )
|
557 | 695 | current_app.config[CONFIG_INGESTER] = ingester
|
558 | 696 |
|
| 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 | + |
559 | 712 | # Used by the OpenAI SDK
|
560 | 713 | openai_client: AsyncOpenAI
|
561 | 714 |
|
@@ -624,6 +777,7 @@ async def setup_clients():
|
624 | 777 | current_app.config[CONFIG_SPEECH_OUTPUT_BROWSER_ENABLED] = USE_SPEECH_OUTPUT_BROWSER
|
625 | 778 | current_app.config[CONFIG_SPEECH_OUTPUT_AZURE_ENABLED] = USE_SPEECH_OUTPUT_AZURE
|
626 | 779 | 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 |
627 | 781 |
|
628 | 782 | # Various approaches to integrate GPT and external knowledge, most applications will use a single one of these patterns
|
629 | 783 | # or some derivative, here we include several for exploration purposes
|
|
0 commit comments