Skip to content

Commit 7a2044a

Browse files
authored
Improve schema of CosmosDB chat history to handle long conversations (#2312)
* Configure Azure Developer Pipeline * CosmosDB v2 changes * CosmosDB progress * Fix CosmosDB API * Revert unneeded changes * Fix tests * Rename message to message_pair * Address Matt's feedback * Add version env var * Reformat with latest black * Minor updates and test fix * Changes based on Marks call * Fix the frontend for the HistoryList API
1 parent a891ab3 commit 7a2044a

File tree

14 files changed

+268
-154
lines changed

14 files changed

+268
-154
lines changed

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ repos:
77
- id: end-of-file-fixer
88
- id: trailing-whitespace
99
- repo: https://github.com/astral-sh/ruff-pre-commit
10-
rev: v0.7.2
10+
rev: v0.9.3
1111
hooks:
1212
- id: ruff
1313
- repo: https://github.com/psf/black
14-
rev: 24.10.0
14+
rev: 25.1.0
1515
hooks:
1616
- id: black
1717
- repo: https://github.com/pre-commit/mirrors-prettier

app/backend/chat_history/cosmosdb.py

Lines changed: 81 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44

55
from azure.cosmos.aio import ContainerProxy, CosmosClient
66
from azure.identity.aio import AzureDeveloperCliCredential, ManagedIdentityCredential
7-
from quart import Blueprint, current_app, jsonify, request
7+
from quart import Blueprint, current_app, jsonify, make_response, request
88

99
from config import (
1010
CONFIG_CHAT_HISTORY_COSMOS_ENABLED,
1111
CONFIG_COSMOS_HISTORY_CLIENT,
1212
CONFIG_COSMOS_HISTORY_CONTAINER,
13+
CONFIG_COSMOS_HISTORY_VERSION,
1314
CONFIG_CREDENTIAL,
1415
)
1516
from decorators import authenticated
@@ -34,23 +35,50 @@ async def post_chat_history(auth_claims: Dict[str, Any]):
3435

3536
try:
3637
request_json = await request.get_json()
37-
id = request_json.get("id")
38-
answers = request_json.get("answers")
39-
title = answers[0][0][:50] + "..." if len(answers[0][0]) > 50 else answers[0][0]
38+
session_id = request_json.get("id")
39+
message_pairs = request_json.get("answers")
40+
first_question = message_pairs[0][0]
41+
title = first_question + "..." if len(first_question) > 50 else first_question
4042
timestamp = int(time.time() * 1000)
4143

42-
await container.upsert_item(
43-
{"id": id, "entra_oid": entra_oid, "title": title, "answers": answers, "timestamp": timestamp}
44-
)
44+
# Insert the session item:
45+
session_item = {
46+
"id": session_id,
47+
"version": current_app.config[CONFIG_COSMOS_HISTORY_VERSION],
48+
"session_id": session_id,
49+
"entra_oid": entra_oid,
50+
"type": "session",
51+
"title": title,
52+
"timestamp": timestamp,
53+
}
54+
55+
message_pair_items = []
56+
# Now insert a message item for each question/response pair:
57+
for ind, message_pair in enumerate(message_pairs):
58+
message_pair_items.append(
59+
{
60+
"id": f"{session_id}-{ind}",
61+
"version": current_app.config[CONFIG_COSMOS_HISTORY_VERSION],
62+
"session_id": session_id,
63+
"entra_oid": entra_oid,
64+
"type": "message_pair",
65+
"question": message_pair[0],
66+
"response": message_pair[1],
67+
}
68+
)
4569

70+
batch_operations = [("upsert", (session_item,))] + [
71+
("upsert", (message_pair_item,)) for message_pair_item in message_pair_items
72+
]
73+
await container.execute_item_batch(batch_operations=batch_operations, partition_key=[entra_oid, session_id])
4674
return jsonify({}), 201
4775
except Exception as error:
4876
return error_response(error, "/chat_history")
4977

5078

51-
@chat_history_cosmosdb_bp.post("/chat_history/items")
79+
@chat_history_cosmosdb_bp.get("/chat_history/sessions")
5280
@authenticated
53-
async def get_chat_history(auth_claims: Dict[str, Any]):
81+
async def get_chat_history_sessions(auth_claims: Dict[str, Any]):
5482
if not current_app.config[CONFIG_CHAT_HISTORY_COSMOS_ENABLED]:
5583
return jsonify({"error": "Chat history not enabled"}), 400
5684

@@ -63,27 +91,26 @@ async def get_chat_history(auth_claims: Dict[str, Any]):
6391
return jsonify({"error": "User OID not found"}), 401
6492

6593
try:
66-
request_json = await request.get_json()
67-
count = request_json.get("count", 20)
68-
continuation_token = request_json.get("continuation_token")
94+
count = int(request.args.get("count", 10))
95+
continuation_token = request.args.get("continuation_token")
6996

7097
res = container.query_items(
71-
query="SELECT c.id, c.entra_oid, c.title, c.timestamp FROM c WHERE c.entra_oid = @entra_oid ORDER BY c.timestamp DESC",
72-
parameters=[dict(name="@entra_oid", value=entra_oid)],
98+
query="SELECT c.id, c.entra_oid, c.title, c.timestamp FROM c WHERE c.entra_oid = @entra_oid AND c.type = @type ORDER BY c.timestamp DESC",
99+
parameters=[dict(name="@entra_oid", value=entra_oid), dict(name="@type", value="session")],
100+
partition_key=[entra_oid],
73101
max_item_count=count,
74102
)
75103

76-
# set the continuation token for the next page
77104
pager = res.by_page(continuation_token)
78105

79106
# Get the first page, and the continuation token
107+
sessions = []
80108
try:
81109
page = await pager.__anext__()
82110
continuation_token = pager.continuation_token # type: ignore
83111

84-
items = []
85112
async for item in page:
86-
items.append(
113+
sessions.append(
87114
{
88115
"id": item.get("id"),
89116
"entra_oid": item.get("entra_oid"),
@@ -94,18 +121,17 @@ async def get_chat_history(auth_claims: Dict[str, Any]):
94121

95122
# If there are no more pages, StopAsyncIteration is raised
96123
except StopAsyncIteration:
97-
items = []
98124
continuation_token = None
99125

100-
return jsonify({"items": items, "continuation_token": continuation_token}), 200
126+
return jsonify({"sessions": sessions, "continuation_token": continuation_token}), 200
101127

102128
except Exception as error:
103-
return error_response(error, "/chat_history/items")
129+
return error_response(error, "/chat_history/sessions")
104130

105131

106-
@chat_history_cosmosdb_bp.get("/chat_history/items/<item_id>")
132+
@chat_history_cosmosdb_bp.get("/chat_history/sessions/<session_id>")
107133
@authenticated
108-
async def get_chat_history_session(auth_claims: Dict[str, Any], item_id: str):
134+
async def get_chat_history_session(auth_claims: Dict[str, Any], session_id: str):
109135
if not current_app.config[CONFIG_CHAT_HISTORY_COSMOS_ENABLED]:
110136
return jsonify({"error": "Chat history not enabled"}), 400
111137

@@ -118,26 +144,34 @@ async def get_chat_history_session(auth_claims: Dict[str, Any], item_id: str):
118144
return jsonify({"error": "User OID not found"}), 401
119145

120146
try:
121-
res = await container.read_item(item=item_id, partition_key=entra_oid)
147+
res = container.query_items(
148+
query="SELECT * FROM c WHERE c.session_id = @session_id AND c.type = @type",
149+
parameters=[dict(name="@session_id", value=session_id), dict(name="@type", value="message_pair")],
150+
partition_key=[entra_oid, session_id],
151+
)
152+
153+
message_pairs = []
154+
async for page in res.by_page():
155+
async for item in page:
156+
message_pairs.append([item["question"], item["response"]])
157+
122158
return (
123159
jsonify(
124160
{
125-
"id": res.get("id"),
126-
"entra_oid": res.get("entra_oid"),
127-
"title": res.get("title", "untitled"),
128-
"timestamp": res.get("timestamp"),
129-
"answers": res.get("answers", []),
161+
"id": session_id,
162+
"entra_oid": entra_oid,
163+
"answers": message_pairs,
130164
}
131165
),
132166
200,
133167
)
134168
except Exception as error:
135-
return error_response(error, f"/chat_history/items/{item_id}")
169+
return error_response(error, f"/chat_history/sessions/{session_id}")
136170

137171

138-
@chat_history_cosmosdb_bp.delete("/chat_history/items/<item_id>")
172+
@chat_history_cosmosdb_bp.delete("/chat_history/sessions/<session_id>")
139173
@authenticated
140-
async def delete_chat_history_session(auth_claims: Dict[str, Any], item_id: str):
174+
async def delete_chat_history_session(auth_claims: Dict[str, Any], session_id: str):
141175
if not current_app.config[CONFIG_CHAT_HISTORY_COSMOS_ENABLED]:
142176
return jsonify({"error": "Chat history not enabled"}), 400
143177

@@ -150,10 +184,22 @@ async def delete_chat_history_session(auth_claims: Dict[str, Any], item_id: str)
150184
return jsonify({"error": "User OID not found"}), 401
151185

152186
try:
153-
await container.delete_item(item=item_id, partition_key=entra_oid)
154-
return jsonify({}), 204
187+
res = container.query_items(
188+
query="SELECT c.id FROM c WHERE c.session_id = @session_id",
189+
parameters=[dict(name="@session_id", value=session_id)],
190+
partition_key=[entra_oid, session_id],
191+
)
192+
193+
ids_to_delete = []
194+
async for page in res.by_page():
195+
async for item in page:
196+
ids_to_delete.append(item["id"])
197+
198+
batch_operations = [("delete", (id,)) for id in ids_to_delete]
199+
await container.execute_item_batch(batch_operations=batch_operations, partition_key=[entra_oid, session_id])
200+
return await make_response("", 204)
155201
except Exception as error:
156-
return error_response(error, f"/chat_history/items/{item_id}")
202+
return error_response(error, f"/chat_history/sessions/{session_id}")
157203

158204

159205
@chat_history_cosmosdb_bp.before_app_serving
@@ -183,6 +229,7 @@ async def setup_clients():
183229

184230
current_app.config[CONFIG_COSMOS_HISTORY_CLIENT] = cosmos_client
185231
current_app.config[CONFIG_COSMOS_HISTORY_CONTAINER] = cosmos_container
232+
current_app.config[CONFIG_COSMOS_HISTORY_VERSION] = os.environ["AZURE_CHAT_HISTORY_VERSION"]
186233

187234

188235
@chat_history_cosmosdb_bp.after_app_serving

app/backend/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@
2626
CONFIG_CHAT_HISTORY_COSMOS_ENABLED = "chat_history_cosmos_enabled"
2727
CONFIG_COSMOS_HISTORY_CLIENT = "cosmos_history_client"
2828
CONFIG_COSMOS_HISTORY_CONTAINER = "cosmos_history_container"
29+
CONFIG_COSMOS_HISTORY_VERSION = "cosmos_history_version"

app/backend/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ azure-core==1.30.2
4747
# msrest
4848
azure-core-tracing-opentelemetry==1.0.0b11
4949
# via azure-monitor-opentelemetry
50-
azure-cosmos==4.7.0
50+
azure-cosmos==4.9.0
5151
# via -r requirements.in
5252
azure-identity==1.17.1
5353
# via

app/frontend/src/api/api.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const BACKEND_URI = "";
22

3-
import { ChatAppResponse, ChatAppResponseOrError, ChatAppRequest, Config, SimpleAPIResponse, HistoryListApiResponse, HistroyApiResponse } from "./models";
3+
import { ChatAppResponse, ChatAppResponseOrError, ChatAppRequest, Config, SimpleAPIResponse, HistoryListApiResponse, HistoryApiResponse } from "./models";
44
import { useLogin, getToken, isUsingAppServicesLogin } from "../authConfig";
55

66
export async function getHeaders(idToken: string | undefined): Promise<Record<string, string>> {
@@ -145,10 +145,14 @@ export async function postChatHistoryApi(item: any, idToken: string): Promise<an
145145

146146
export async function getChatHistoryListApi(count: number, continuationToken: string | undefined, idToken: string): Promise<HistoryListApiResponse> {
147147
const headers = await getHeaders(idToken);
148-
const response = await fetch("/chat_history/items", {
149-
method: "POST",
150-
headers: { ...headers, "Content-Type": "application/json" },
151-
body: JSON.stringify({ count: count, continuation_token: continuationToken })
148+
let url = `${BACKEND_URI}/chat_history/sessions?count=${count}`;
149+
if (continuationToken) {
150+
url += `&continuationToken=${continuationToken}`;
151+
}
152+
153+
const response = await fetch(url.toString(), {
154+
method: "GET",
155+
headers: { ...headers, "Content-Type": "application/json" }
152156
});
153157

154158
if (!response.ok) {
@@ -159,9 +163,9 @@ export async function getChatHistoryListApi(count: number, continuationToken: st
159163
return dataResponse;
160164
}
161165

162-
export async function getChatHistoryApi(id: string, idToken: string): Promise<HistroyApiResponse> {
166+
export async function getChatHistoryApi(id: string, idToken: string): Promise<HistoryApiResponse> {
163167
const headers = await getHeaders(idToken);
164-
const response = await fetch(`/chat_history/items/${id}`, {
168+
const response = await fetch(`/chat_history/sessions/${id}`, {
165169
method: "GET",
166170
headers: { ...headers, "Content-Type": "application/json" }
167171
});
@@ -170,21 +174,18 @@ export async function getChatHistoryApi(id: string, idToken: string): Promise<Hi
170174
throw new Error(`Getting chat history failed: ${response.statusText}`);
171175
}
172176

173-
const dataResponse: HistroyApiResponse = await response.json();
177+
const dataResponse: HistoryApiResponse = await response.json();
174178
return dataResponse;
175179
}
176180

177181
export async function deleteChatHistoryApi(id: string, idToken: string): Promise<any> {
178182
const headers = await getHeaders(idToken);
179-
const response = await fetch(`/chat_history/items/${id}`, {
183+
const response = await fetch(`/chat_history/sessions/${id}`, {
180184
method: "DELETE",
181185
headers: { ...headers, "Content-Type": "application/json" }
182186
});
183187

184188
if (!response.ok) {
185189
throw new Error(`Deleting chat history failed: ${response.statusText}`);
186190
}
187-
188-
const dataResponse: any = await response.json();
189-
return dataResponse;
190191
}

app/frontend/src/api/models.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export interface SpeechConfig {
107107
}
108108

109109
export type HistoryListApiResponse = {
110-
items: {
110+
sessions: {
111111
id: string;
112112
entra_oid: string;
113113
title: string;
@@ -116,10 +116,8 @@ export type HistoryListApiResponse = {
116116
continuation_token?: string;
117117
};
118118

119-
export type HistroyApiResponse = {
119+
export type HistoryApiResponse = {
120120
id: string;
121121
entra_oid: string;
122-
title: string;
123122
answers: any;
124-
timestamp: number;
125123
};

app/frontend/src/components/HistoryProviders/CosmosDB.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ export class CosmosDBProvider implements IHistoryProvider {
2323
if (!this.continuationToken) {
2424
this.isItemEnd = true;
2525
}
26-
return response.items.map(item => ({
27-
id: item.id,
28-
title: item.title,
29-
timestamp: item.timestamp
26+
return response.sessions.map(session => ({
27+
id: session.id,
28+
title: session.title,
29+
timestamp: session.timestamp
3030
}));
3131
} catch (e) {
3232
console.error(e);

0 commit comments

Comments
 (0)