Skip to content

Commit 4681f80

Browse files
committed
Fix CosmosDB API
1 parent e0f01db commit 4681f80

File tree

5 files changed

+68
-82
lines changed

5 files changed

+68
-82
lines changed

app/backend/chat_history/cosmosdb.py

Lines changed: 58 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@
55

66
from azure.cosmos import exceptions
77
from azure.cosmos.aio import ContainerProxy, CosmosClient
8-
from azure.cosmos.partition_key import PartitionKey
98
from azure.identity.aio import AzureDeveloperCliCredential, ManagedIdentityCredential
10-
from quart import Blueprint, current_app, jsonify, request
9+
from quart import Blueprint, current_app, jsonify, make_response, request
1110

1211
from config import (
1312
CONFIG_CHAT_HISTORY_COSMOS_ENABLED,
@@ -23,14 +22,6 @@
2322
chat_history_cosmosdb_bp = Blueprint("chat_history_cosmos", __name__, static_folder="static")
2423

2524

26-
def make_partition_key(entra_id, session_id=None):
27-
if entra_id and session_id:
28-
# Need multihash for hierachical partitioning
29-
return PartitionKey(path=["/entra_id", "/session_id"], kind="MultiHash")
30-
else:
31-
return PartitionKey(path="/entra_id")
32-
33-
3425
@chat_history_cosmosdb_bp.post("/chat_history")
3526
@authenticated
3627
async def post_chat_history(auth_claims: Dict[str, Any]):
@@ -48,56 +39,40 @@ async def post_chat_history(auth_claims: Dict[str, Any]):
4839
try:
4940
request_json = await request.get_json()
5041
session_id = request_json.get("id")
51-
answers = request_json.get("answers")
52-
title = answers[0][0][:50] + "..." if len(answers[0][0]) > 50 else answers[0][0]
42+
message_pairs = request_json.get("answers")
43+
first_message_question = message_pairs[0][0]
44+
title = first_message_question + "..." if len(first_message_question) > 50 else first_message_question
5345
timestamp = int(time.time() * 1000)
5446

5547
# Insert the session item:
56-
session_item = {
57-
"id": id,
48+
session = {
49+
"id": session_id,
5850
"session_id": session_id,
5951
"entra_oid": entra_oid,
6052
"type": "session",
6153
"title": title,
6254
"timestamp": timestamp,
6355
}
6456

65-
message_items = []
57+
messages = []
6658
# Now insert a message item for each question/response pair:
67-
for ind, message_pair in enumerate(zip(answers[::2], answers[1::2])):
68-
# The id: what if you delete a message and then add a new one? The id will be the same.
69-
# If we had delete mechanism, and you deleted item 5 in a history, then item 6 would still hang around
70-
# and youd have two of item 6.
71-
# abc-0
72-
# abc-1
73-
# abc-2 <-- DELETE
74-
# abc-3
75-
# One approach would be to delete EVERYTHING, then upsert everything.
76-
# Another approach would be to delete item plus everything after, then upsert everything after.
77-
# Or: Change the frontend?
78-
# We can do this first, and change the frontend after
79-
message_items.append(
59+
for ind, message_pair in enumerate(message_pairs):
60+
messages.append(
8061
{
8162
"id": f"{session_id}-{ind}",
82-
"session_id": id,
63+
"session_id": session_id,
8364
"entra_oid": entra_oid,
8465
"type": "message",
8566
"question": message_pair[0],
8667
"response": message_pair[1],
87-
"timestamp": timestamp, # <-- This is the timestamp of the session, not the message
68+
"timestamp": None,
8869
}
8970
)
9071

91-
batch_operations = [("upsert", tuple([session_item] + message_items), {})]
72+
batch_operations = [("upsert", (session,))] + [("upsert", (message,)) for message in messages]
9273

9374
try:
94-
# Run that list of operations
95-
batch_results = container.execute_item_batch(
96-
batch_operations=batch_operations, partition_key=make_partition_key(entra_oid, session_id)
97-
)
98-
# Batch results are returned as a list of item operation results - or raise a CosmosBatchOperationError if
99-
# one of the operations failed within your batch request.
100-
print(f"\nResults for the batch operations: {batch_results}\n")
75+
await container.execute_item_batch(batch_operations=batch_operations, partition_key=[entra_oid, session_id])
10176
except exceptions.CosmosBatchOperationError as e:
10277
error_operation_index = e.error_index
10378
error_operation_response = e.operation_responses[error_operation_index]
@@ -109,9 +84,9 @@ async def post_chat_history(auth_claims: Dict[str, Any]):
10984
return error_response(error, "/chat_history")
11085

11186

112-
@chat_history_cosmosdb_bp.get("/chat_history/items")
87+
@chat_history_cosmosdb_bp.get("/chat_history/sessions")
11388
@authenticated
114-
async def get_chat_history(auth_claims: Dict[str, Any]):
89+
async def get_chat_history_sessions(auth_claims: Dict[str, Any]):
11590
if not current_app.config[CONFIG_CHAT_HISTORY_COSMOS_ENABLED]:
11691
return jsonify({"error": "Chat history not enabled"}), 400
11792

@@ -125,28 +100,27 @@ async def get_chat_history(auth_claims: Dict[str, Any]):
125100

126101
try:
127102
# get the count and continuation token from the request URL
128-
count = request.args.get("count", 10)
103+
count = int(request.args.get("count", 10))
129104
continuation_token = request.args.get("continuation_token")
130105

131106
res = container.query_items(
132-
# TODO: do we need distinct? per Mark's code - Mark says no!
133107
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",
134108
parameters=[dict(name="@entra_oid", value=entra_oid), dict(name="@type", value="session")],
135-
partition_key=make_partition_key(entra_oid),
109+
partition_key=[entra_oid],
136110
max_item_count=count,
137111
)
138112

139113
# set the continuation token for the next page
140114
pager = res.by_page(continuation_token)
141115

142116
# Get the first page, and the continuation token
117+
sessions = []
143118
try:
144119
page = await pager.__anext__()
145120
continuation_token = pager.continuation_token # type: ignore
146121

147-
items = []
148122
async for item in page:
149-
items.append(
123+
sessions.append(
150124
{
151125
"id": item.get("id"),
152126
"entra_oid": item.get("entra_oid"),
@@ -157,18 +131,17 @@ async def get_chat_history(auth_claims: Dict[str, Any]):
157131

158132
# If there are no more pages, StopAsyncIteration is raised
159133
except StopAsyncIteration:
160-
items = []
161134
continuation_token = None
162135

163-
return jsonify({"items": items, "continuation_token": continuation_token}), 200
136+
return jsonify({"sessions": sessions, "continuation_token": continuation_token}), 200
164137

165138
except Exception as error:
166-
return error_response(error, "/chat_history/items")
139+
return error_response(error, "/chat_history/sessions")
167140

168141

169-
@chat_history_cosmosdb_bp.get("/chat_history/items/<item_id>")
142+
@chat_history_cosmosdb_bp.get("/chat_history/sessions/<session_id>")
170143
@authenticated
171-
async def get_chat_history_session(auth_claims: Dict[str, Any], item_id: str):
144+
async def get_chat_history_session(auth_claims: Dict[str, Any], session_id: str):
172145
if not current_app.config[CONFIG_CHAT_HISTORY_COSMOS_ENABLED]:
173146
return jsonify({"error": "Chat history not enabled"}), 400
174147

@@ -182,33 +155,39 @@ async def get_chat_history_session(auth_claims: Dict[str, Any], item_id: str):
182155

183156
try:
184157
res = container.query_items(
185-
# TODO: do we need distinct? per Mark's code
186-
query="SELECT c.id, c.entra_oid, c.title, c.timestamp FROM c WHERE c.session_id = @session_id ORDER BY c.timestamp DESC",
187-
parameters=[dict(name="@entra_oid", value=entra_oid), dict(name="@session_id", value=item_id)],
188-
partition_key=make_partition_key(entra_oid, item_id),
189-
# max_item_count=?
158+
query="SELECT * FROM c WHERE c.session_id = @session_id",
159+
parameters=[dict(name="@entra_oid", value=entra_oid), dict(name="@session_id", value=session_id)],
160+
partition_key=[entra_oid, session_id],
190161
)
191162

192-
res = await container.read_item(item=item_id, partition_key=entra_oid)
163+
message_pairs = []
164+
session = None
165+
async for page in res.by_page():
166+
async for item in page:
167+
if item.get("type") == "session":
168+
session = item
169+
else:
170+
message_pairs.append([item["question"], item["response"]])
171+
193172
return (
194173
jsonify(
195174
{
196-
"id": res.get("id"),
197-
"entra_oid": res.get("entra_oid"),
198-
"title": res.get("title", "untitled"),
199-
"timestamp": res.get("timestamp"),
200-
"answers": res.get("answers", []),
175+
"id": session.get("id"),
176+
"entra_oid": entra_oid,
177+
"title": session.get("title"),
178+
"timestamp": session.get("timestamp"),
179+
"answers": message_pairs,
201180
}
202181
),
203182
200,
204183
)
205184
except Exception as error:
206-
return error_response(error, f"/chat_history/items/{item_id}")
185+
return error_response(error, f"/chat_history/sessions/{session_id}")
207186

208187

209-
@chat_history_cosmosdb_bp.delete("/chat_history/items/<item_id>")
188+
@chat_history_cosmosdb_bp.delete("/chat_history/sessions/<session_id>")
210189
@authenticated
211-
async def delete_chat_history_session(auth_claims: Dict[str, Any], item_id: str):
190+
async def delete_chat_history_session(auth_claims: Dict[str, Any], session_id: str):
212191
if not current_app.config[CONFIG_CHAT_HISTORY_COSMOS_ENABLED]:
213192
return jsonify({"error": "Chat history not enabled"}), 400
214193

@@ -221,12 +200,22 @@ async def delete_chat_history_session(auth_claims: Dict[str, Any], item_id: str)
221200
return jsonify({"error": "User OID not found"}), 401
222201

223202
try:
224-
await container.delete_item(item=item_id, partition_key=entra_oid)
225-
# Delete session, and all the message items associated with it
226-
# TODO: Delete all the message items as well
227-
return jsonify({}), 204
203+
res = container.query_items(
204+
query="SELECT * FROM c WHERE c.session_id = @session_id",
205+
parameters=[dict(name="@entra_oid", value=entra_oid), dict(name="@session_id", value=session_id)],
206+
partition_key=[entra_oid, session_id],
207+
)
208+
209+
ids_to_delete = []
210+
async for page in res.by_page():
211+
async for item in page:
212+
ids_to_delete.append(item["id"])
213+
214+
batch_operations = [("delete", (id,)) for id in ids_to_delete]
215+
await container.execute_item_batch(batch_operations=batch_operations, partition_key=[entra_oid, session_id])
216+
return make_response("", 204)
228217
except Exception as error:
229-
return error_response(error, f"/chat_history/items/{item_id}")
218+
return error_response(error, f"/chat_history/sessions/{session_id}")
230219

231220

232221
@chat_history_cosmosdb_bp.before_app_serving

app/frontend/src/api/api.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,9 @@ 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 url = new URL("/chat_history/items", BACKEND_URI);
149-
url.searchParams.append("count", count.toString());
148+
let url = `${BACKEND_URI}/chat_history/sessions?count=${count}`;
150149
if (continuationToken) {
151-
url.searchParams.append("continuationToken", continuationToken);
150+
url += `&continuationToken=${continuationToken}`;
152151
}
153152

154153
const response = await fetch(url.toString(), {
@@ -166,7 +165,7 @@ export async function getChatHistoryListApi(count: number, continuationToken: st
166165

167166
export async function getChatHistoryApi(id: string, idToken: string): Promise<HistroyApiResponse> {
168167
const headers = await getHeaders(idToken);
169-
const response = await fetch(`/chat_history/items/${id}`, {
168+
const response = await fetch(`/chat_history/sessions/${id}`, {
170169
method: "GET",
171170
headers: { ...headers, "Content-Type": "application/json" }
172171
});
@@ -181,15 +180,12 @@ export async function getChatHistoryApi(id: string, idToken: string): Promise<Hi
181180

182181
export async function deleteChatHistoryApi(id: string, idToken: string): Promise<any> {
183182
const headers = await getHeaders(idToken);
184-
const response = await fetch(`/chat_history/items/${id}`, {
183+
const response = await fetch(`/chat_history/sessions/${id}`, {
185184
method: "DELETE",
186185
headers: { ...headers, "Content-Type": "application/json" }
187186
});
188187

189188
if (!response.ok) {
190189
throw new Error(`Deleting chat history failed: ${response.statusText}`);
191190
}
192-
193-
const dataResponse: any = await response.json();
194-
return dataResponse;
195191
}

app/frontend/src/api/models.ts

Lines changed: 1 addition & 1 deletion
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;

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);

infra/main.bicep

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,7 @@ module cosmosDb 'br/public:avm/res/document-db/database-account:0.6.1' = if (use
799799
containers: [
800800
{
801801
name: chatHistoryContainerName
802+
kind: 'MultiHash'
802803
paths: [
803804
'/entra_oid'
804805
'/session_id'

0 commit comments

Comments
 (0)