From e46dc9f36e9646bf329feebccc3ec26f282c0a06 Mon Sep 17 00:00:00 2001 From: Pamela Fox Date: Wed, 29 Jan 2025 14:16:21 -0800 Subject: [PATCH] Changes based on Marks call --- app/backend/chat_history/cosmosdb.py | 20 +- app/backend/requirements.txt | 2 +- app/frontend/src/api/api.ts | 340 ++++++++++-------- app/frontend/src/api/models.ts | 152 ++++---- infra/main.bicep | 3 - .../auth_public_documents_client0/result.json | 4 +- tests/test_cosmosdb.py | 34 +- 7 files changed, 297 insertions(+), 258 deletions(-) diff --git a/app/backend/chat_history/cosmosdb.py b/app/backend/chat_history/cosmosdb.py index 9547e66ce2..fca1585b93 100644 --- a/app/backend/chat_history/cosmosdb.py +++ b/app/backend/chat_history/cosmosdb.py @@ -64,15 +64,12 @@ async def post_chat_history(auth_claims: Dict[str, Any]): "type": "message_pair", "question": message_pair[0], "response": message_pair[1], - "order": ind, - "timestamp": None, } ) batch_operations = [("upsert", (session_item,))] + [ ("upsert", (message_pair_item,)) for message_pair_item in message_pair_items ] - await container.execute_item_batch(batch_operations=batch_operations, partition_key=[entra_oid, session_id]) return jsonify({}), 201 except Exception as error: @@ -148,30 +145,21 @@ async def get_chat_history_session(auth_claims: Dict[str, Any], session_id: str) try: res = container.query_items( - query="SELECT * FROM c WHERE c.session_id = @session_id", - parameters=[dict(name="@session_id", value=session_id)], + query="SELECT * FROM c WHERE c.session_id = @session_id AND c.type = @type", + parameters=[dict(name="@session_id", value=session_id), dict(name="@type", value="message_pair")], partition_key=[entra_oid, session_id], ) message_pairs = [] - session = None async for page in res.by_page(): async for item in page: - if item.get("type") == "session": - session = item - elif item.get("type") == "message_pair": - message_pairs.append([item["question"], item["response"]]) - - if session is None: - return jsonify({"error": "Session not found"}), 404 + message_pairs.append([item["question"], item["response"]]) return ( jsonify( { - "id": session.get("id"), + "id": session_id, "entra_oid": entra_oid, - "title": session.get("title"), - "timestamp": session.get("timestamp"), "answers": message_pairs, } ), diff --git a/app/backend/requirements.txt b/app/backend/requirements.txt index 5712aae47e..17a44aa8a3 100644 --- a/app/backend/requirements.txt +++ b/app/backend/requirements.txt @@ -47,7 +47,7 @@ azure-core==1.30.2 # msrest azure-core-tracing-opentelemetry==1.0.0b11 # via azure-monitor-opentelemetry -azure-cosmos==4.7.0 +azure-cosmos==4.9.0 # via -r requirements.in azure-identity==1.17.1 # via diff --git a/app/frontend/src/api/api.ts b/app/frontend/src/api/api.ts index cef74125ba..40fc53f69e 100644 --- a/app/frontend/src/api/api.ts +++ b/app/frontend/src/api/api.ts @@ -1,191 +1,227 @@ const BACKEND_URI = ""; -import { ChatAppResponse, ChatAppResponseOrError, ChatAppRequest, Config, SimpleAPIResponse, HistoryListApiResponse, HistroyApiResponse } from "./models"; +import { + ChatAppResponse, + ChatAppResponseOrError, + ChatAppRequest, + Config, + SimpleAPIResponse, + HistoryListApiResponse, + HistoryApiResponse, +} from "./models"; import { useLogin, getToken, isUsingAppServicesLogin } from "../authConfig"; -export async function getHeaders(idToken: string | undefined): Promise> { - // If using login and not using app services, add the id token of the logged in account as the authorization - if (useLogin && !isUsingAppServicesLogin) { - if (idToken) { - return { Authorization: `Bearer ${idToken}` }; - } +export async function getHeaders( + idToken: string | undefined, +): Promise> { + // If using login and not using app services, add the id token of the logged in account as the authorization + if (useLogin && !isUsingAppServicesLogin) { + if (idToken) { + return { Authorization: `Bearer ${idToken}` }; } + } - return {}; + return {}; } export async function configApi(): Promise { - const response = await fetch(`${BACKEND_URI}/config`, { - method: "GET" - }); + const response = await fetch(`${BACKEND_URI}/config`, { + method: "GET", + }); - return (await response.json()) as Config; + return (await response.json()) as Config; } -export async function askApi(request: ChatAppRequest, idToken: string | undefined): Promise { - const headers = await getHeaders(idToken); - const response = await fetch(`${BACKEND_URI}/ask`, { - method: "POST", - headers: { ...headers, "Content-Type": "application/json" }, - body: JSON.stringify(request) - }); - - if (response.status > 299 || !response.ok) { - throw Error(`Request failed with status ${response.status}`); - } - const parsedResponse: ChatAppResponseOrError = await response.json(); - if (parsedResponse.error) { - throw Error(parsedResponse.error); - } - - return parsedResponse as ChatAppResponse; +export async function askApi( + request: ChatAppRequest, + idToken: string | undefined, +): Promise { + const headers = await getHeaders(idToken); + const response = await fetch(`${BACKEND_URI}/ask`, { + method: "POST", + headers: { ...headers, "Content-Type": "application/json" }, + body: JSON.stringify(request), + }); + + if (response.status > 299 || !response.ok) { + throw Error(`Request failed with status ${response.status}`); + } + const parsedResponse: ChatAppResponseOrError = await response.json(); + if (parsedResponse.error) { + throw Error(parsedResponse.error); + } + + return parsedResponse as ChatAppResponse; } -export async function chatApi(request: ChatAppRequest, shouldStream: boolean, idToken: string | undefined): Promise { - let url = `${BACKEND_URI}/chat`; - if (shouldStream) { - url += "/stream"; - } - const headers = await getHeaders(idToken); - return await fetch(url, { - method: "POST", - headers: { ...headers, "Content-Type": "application/json" }, - body: JSON.stringify(request) - }); +export async function chatApi( + request: ChatAppRequest, + shouldStream: boolean, + idToken: string | undefined, +): Promise { + let url = `${BACKEND_URI}/chat`; + if (shouldStream) { + url += "/stream"; + } + const headers = await getHeaders(idToken); + return await fetch(url, { + method: "POST", + headers: { ...headers, "Content-Type": "application/json" }, + body: JSON.stringify(request), + }); } export async function getSpeechApi(text: string): Promise { - return await fetch("/speech", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - text: text - }) + return await fetch("/speech", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + text: text, + }), + }) + .then((response) => { + if (response.status == 200) { + return response.blob(); + } else if (response.status == 400) { + console.log("Speech synthesis is not enabled."); + return null; + } else { + console.error("Unable to get speech synthesis."); + return null; + } }) - .then(response => { - if (response.status == 200) { - return response.blob(); - } else if (response.status == 400) { - console.log("Speech synthesis is not enabled."); - return null; - } else { - console.error("Unable to get speech synthesis."); - return null; - } - }) - .then(blob => (blob ? URL.createObjectURL(blob) : null)); + .then((blob) => (blob ? URL.createObjectURL(blob) : null)); } export function getCitationFilePath(citation: string): string { - return `${BACKEND_URI}/content/${citation}`; + return `${BACKEND_URI}/content/${citation}`; } -export async function uploadFileApi(request: FormData, idToken: string): Promise { - const response = await fetch("/upload", { - method: "POST", - headers: await getHeaders(idToken), - body: request - }); - - if (!response.ok) { - throw new Error(`Uploading files failed: ${response.statusText}`); - } - - const dataResponse: SimpleAPIResponse = await response.json(); - return dataResponse; +export async function uploadFileApi( + request: FormData, + idToken: string, +): Promise { + const response = await fetch("/upload", { + method: "POST", + headers: await getHeaders(idToken), + body: request, + }); + + if (!response.ok) { + throw new Error(`Uploading files failed: ${response.statusText}`); + } + + const dataResponse: SimpleAPIResponse = await response.json(); + return dataResponse; } -export async function deleteUploadedFileApi(filename: string, idToken: string): Promise { - const headers = await getHeaders(idToken); - const response = await fetch("/delete_uploaded", { - method: "POST", - headers: { ...headers, "Content-Type": "application/json" }, - body: JSON.stringify({ filename }) - }); - - if (!response.ok) { - throw new Error(`Deleting file failed: ${response.statusText}`); - } - - const dataResponse: SimpleAPIResponse = await response.json(); - return dataResponse; +export async function deleteUploadedFileApi( + filename: string, + idToken: string, +): Promise { + const headers = await getHeaders(idToken); + const response = await fetch("/delete_uploaded", { + method: "POST", + headers: { ...headers, "Content-Type": "application/json" }, + body: JSON.stringify({ filename }), + }); + + if (!response.ok) { + throw new Error(`Deleting file failed: ${response.statusText}`); + } + + const dataResponse: SimpleAPIResponse = await response.json(); + return dataResponse; } export async function listUploadedFilesApi(idToken: string): Promise { - const response = await fetch(`/list_uploaded`, { - method: "GET", - headers: await getHeaders(idToken) - }); + const response = await fetch(`/list_uploaded`, { + method: "GET", + headers: await getHeaders(idToken), + }); - if (!response.ok) { - throw new Error(`Listing files failed: ${response.statusText}`); - } + if (!response.ok) { + throw new Error(`Listing files failed: ${response.statusText}`); + } - const dataResponse: string[] = await response.json(); - return dataResponse; + const dataResponse: string[] = await response.json(); + return dataResponse; } -export async function postChatHistoryApi(item: any, idToken: string): Promise { - const headers = await getHeaders(idToken); - const response = await fetch("/chat_history", { - method: "POST", - headers: { ...headers, "Content-Type": "application/json" }, - body: JSON.stringify(item) - }); - - if (!response.ok) { - throw new Error(`Posting chat history failed: ${response.statusText}`); - } - - const dataResponse: any = await response.json(); - return dataResponse; +export async function postChatHistoryApi( + item: any, + idToken: string, +): Promise { + const headers = await getHeaders(idToken); + const response = await fetch("/chat_history", { + method: "POST", + headers: { ...headers, "Content-Type": "application/json" }, + body: JSON.stringify(item), + }); + + if (!response.ok) { + throw new Error(`Posting chat history failed: ${response.statusText}`); + } + + const dataResponse: any = await response.json(); + return dataResponse; } -export async function getChatHistoryListApi(count: number, continuationToken: string | undefined, idToken: string): Promise { - const headers = await getHeaders(idToken); - let url = `${BACKEND_URI}/chat_history/sessions?count=${count}`; - if (continuationToken) { - url += `&continuationToken=${continuationToken}`; - } - - const response = await fetch(url.toString(), { - method: "GET", - headers: { ...headers, "Content-Type": "application/json" } - }); - - if (!response.ok) { - throw new Error(`Getting chat histories failed: ${response.statusText}`); - } - - const dataResponse: HistoryListApiResponse = await response.json(); - return dataResponse; +export async function getChatHistoryListApi( + count: number, + continuationToken: string | undefined, + idToken: string, +): Promise { + const headers = await getHeaders(idToken); + let url = `${BACKEND_URI}/chat_history/sessions?count=${count}`; + if (continuationToken) { + url += `&continuationToken=${continuationToken}`; + } + + const response = await fetch(url.toString(), { + method: "GET", + headers: { ...headers, "Content-Type": "application/json" }, + }); + + if (!response.ok) { + throw new Error(`Getting chat histories failed: ${response.statusText}`); + } + + const dataResponse: HistoryListApiResponse = await response.json(); + return dataResponse; } -export async function getChatHistoryApi(id: string, idToken: string): Promise { - const headers = await getHeaders(idToken); - const response = await fetch(`/chat_history/sessions/${id}`, { - method: "GET", - headers: { ...headers, "Content-Type": "application/json" } - }); - - if (!response.ok) { - throw new Error(`Getting chat history failed: ${response.statusText}`); - } - - const dataResponse: HistroyApiResponse = await response.json(); - return dataResponse; +export async function getChatHistoryApi( + id: string, + idToken: string, +): Promise { + const headers = await getHeaders(idToken); + const response = await fetch(`/chat_history/sessions/${id}`, { + method: "GET", + headers: { ...headers, "Content-Type": "application/json" }, + }); + + if (!response.ok) { + throw new Error(`Getting chat history failed: ${response.statusText}`); + } + + const dataResponse: HistoryApiResponse = await response.json(); + return dataResponse; } -export async function deleteChatHistoryApi(id: string, idToken: string): Promise { - const headers = await getHeaders(idToken); - const response = await fetch(`/chat_history/sessions/${id}`, { - method: "DELETE", - headers: { ...headers, "Content-Type": "application/json" } - }); - - if (!response.ok) { - throw new Error(`Deleting chat history failed: ${response.statusText}`); - } +export async function deleteChatHistoryApi( + id: string, + idToken: string, +): Promise { + const headers = await getHeaders(idToken); + const response = await fetch(`/chat_history/sessions/${id}`, { + method: "DELETE", + headers: { ...headers, "Content-Type": "application/json" }, + }); + + if (!response.ok) { + throw new Error(`Deleting chat history failed: ${response.statusText}`); + } } diff --git a/app/frontend/src/api/models.ts b/app/frontend/src/api/models.ts index c3d26fb87f..008912c34c 100644 --- a/app/frontend/src/api/models.ts +++ b/app/frontend/src/api/models.ts @@ -1,125 +1,123 @@ export const enum RetrievalMode { - Hybrid = "hybrid", - Vectors = "vectors", - Text = "text" + Hybrid = "hybrid", + Vectors = "vectors", + Text = "text", } export const enum GPT4VInput { - TextAndImages = "textAndImages", - Images = "images", - Texts = "texts" + TextAndImages = "textAndImages", + Images = "images", + Texts = "texts", } export const enum VectorFieldOptions { - Embedding = "embedding", - ImageEmbedding = "imageEmbedding", - Both = "both" + Embedding = "embedding", + ImageEmbedding = "imageEmbedding", + Both = "both", } export type ChatAppRequestOverrides = { - retrieval_mode?: RetrievalMode; - semantic_ranker?: boolean; - semantic_captions?: boolean; - include_category?: string; - exclude_category?: string; - seed?: number; - top?: number; - temperature?: number; - minimum_search_score?: number; - minimum_reranker_score?: number; - prompt_template?: string; - prompt_template_prefix?: string; - prompt_template_suffix?: string; - suggest_followup_questions?: boolean; - use_oid_security_filter?: boolean; - use_groups_security_filter?: boolean; - use_gpt4v?: boolean; - gpt4v_input?: GPT4VInput; - vector_fields: VectorFieldOptions[]; - language: string; + retrieval_mode?: RetrievalMode; + semantic_ranker?: boolean; + semantic_captions?: boolean; + include_category?: string; + exclude_category?: string; + seed?: number; + top?: number; + temperature?: number; + minimum_search_score?: number; + minimum_reranker_score?: number; + prompt_template?: string; + prompt_template_prefix?: string; + prompt_template_suffix?: string; + suggest_followup_questions?: boolean; + use_oid_security_filter?: boolean; + use_groups_security_filter?: boolean; + use_gpt4v?: boolean; + gpt4v_input?: GPT4VInput; + vector_fields: VectorFieldOptions[]; + language: string; }; export type ResponseMessage = { - content: string; - role: string; + content: string; + role: string; }; export type Thoughts = { - title: string; - description: any; // It can be any output from the api - props?: { [key: string]: string }; + title: string; + description: any; // It can be any output from the api + props?: { [key: string]: string }; }; export type ResponseContext = { - data_points: string[]; - followup_questions: string[] | null; - thoughts: Thoughts[]; + data_points: string[]; + followup_questions: string[] | null; + thoughts: Thoughts[]; }; export type ChatAppResponseOrError = { - message: ResponseMessage; - delta: ResponseMessage; - context: ResponseContext; - session_state: any; - error?: string; + message: ResponseMessage; + delta: ResponseMessage; + context: ResponseContext; + session_state: any; + error?: string; }; export type ChatAppResponse = { - message: ResponseMessage; - delta: ResponseMessage; - context: ResponseContext; - session_state: any; + message: ResponseMessage; + delta: ResponseMessage; + context: ResponseContext; + session_state: any; }; export type ChatAppRequestContext = { - overrides?: ChatAppRequestOverrides; + overrides?: ChatAppRequestOverrides; }; export type ChatAppRequest = { - messages: ResponseMessage[]; - context?: ChatAppRequestContext; - session_state: any; + messages: ResponseMessage[]; + context?: ChatAppRequestContext; + session_state: any; }; export type Config = { - showGPT4VOptions: boolean; - showSemanticRankerOption: boolean; - showVectorOption: boolean; - showUserUpload: boolean; - showLanguagePicker: boolean; - showSpeechInput: boolean; - showSpeechOutputBrowser: boolean; - showSpeechOutputAzure: boolean; - showChatHistoryBrowser: boolean; - showChatHistoryCosmos: boolean; + showGPT4VOptions: boolean; + showSemanticRankerOption: boolean; + showVectorOption: boolean; + showUserUpload: boolean; + showLanguagePicker: boolean; + showSpeechInput: boolean; + showSpeechOutputBrowser: boolean; + showSpeechOutputAzure: boolean; + showChatHistoryBrowser: boolean; + showChatHistoryCosmos: boolean; }; export type SimpleAPIResponse = { - message?: string; + message?: string; }; export interface SpeechConfig { - speechUrls: (string | null)[]; - setSpeechUrls: (urls: (string | null)[]) => void; - audio: HTMLAudioElement; - isPlaying: boolean; - setIsPlaying: (isPlaying: boolean) => void; + speechUrls: (string | null)[]; + setSpeechUrls: (urls: (string | null)[]) => void; + audio: HTMLAudioElement; + isPlaying: boolean; + setIsPlaying: (isPlaying: boolean) => void; } export type HistoryListApiResponse = { - sessions: { - id: string; - entra_oid: string; - title: string; - timestamp: number; - }[]; - continuation_token?: string; -}; - -export type HistroyApiResponse = { + sessions: { id: string; entra_oid: string; title: string; - answers: any; timestamp: number; + }[]; + continuation_token?: string; +}; + +export type HistoryApiResponse = { + id: string; + entra_oid: string; + answers: any; }; diff --git a/infra/main.bicep b/infra/main.bicep index 26d2320bd6..e52a38c09d 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -822,9 +822,6 @@ module cosmosDb 'br/public:avm/res/document-db/database-account:0.6.1' = if (use { path: '/type/?' } - { - path: '/order/?' - } ] excludedPaths: [ { diff --git a/tests/snapshots/test_cosmosdb/test_chathistory_getitem/auth_public_documents_client0/result.json b/tests/snapshots/test_cosmosdb/test_chathistory_getitem/auth_public_documents_client0/result.json index 18f2ed3d37..ab59969d83 100644 --- a/tests/snapshots/test_cosmosdb/test_chathistory_getitem/auth_public_documents_client0/result.json +++ b/tests/snapshots/test_cosmosdb/test_chathistory_getitem/auth_public_documents_client0/result.json @@ -15,7 +15,5 @@ ] ], "entra_oid": "OID_X", - "id": "123", - "timestamp": 1738174630204, - "title": "What does a Product Manager do?" + "id": "123" } \ No newline at end of file diff --git a/tests/test_cosmosdb.py b/tests/test_cosmosdb.py index 9a6e201d76..6efc9b7f3a 100644 --- a/tests/test_cosmosdb.py +++ b/tests/test_cosmosdb.py @@ -19,16 +19,15 @@ ] ] -for_session_id_query = [ +for_deletion_query = [ [ { "id": "123", - "version": "cosmosdb-v2", "session_id": "123", "entra_oid": "OID_X", + "title": "This is a test message", + "timestamp": 123456789, "type": "session", - "title": "What does a Product Manager do?", - "timestamp": 1738174630204, }, { "id": "123-0", @@ -51,6 +50,29 @@ ] ] +for_message_pairs_query = [ + [ + { + "id": "123-0", + "version": "cosmosdb-v2", + "session_id": "123", + "entra_oid": "OID_X", + "type": "message_pair", + "question": "What does a Product Manager do?", + "response": { + "delta": {"role": "assistant"}, + "session_state": "143c0240-b2ee-4090-8e90-2a1c58124894", + "message": { + "content": "A Product Manager is responsible for leading the product management team and providing guidance on product strategy, design, development, and launch. They collaborate with internal teams and external partners to ensure successful product execution. They also develop and implement product life-cycle management processes, monitor industry trends, develop product marketing plans, research customer needs, collaborate with internal teams, develop pricing strategies, oversee product portfolio, analyze product performance, and identify areas for improvement [role_library.pdf#page=29][role_library.pdf#page=12][role_library.pdf#page=23].", + "role": "assistant", + }, + }, + "order": 0, + "timestamp": None, + }, + ] +] + class MockCosmosDBResultsIterator: def __init__(self, data=[]): @@ -248,7 +270,7 @@ def mock_query_items(container_proxy, query, **kwargs): async def test_chathistory_getitem(auth_public_documents_client, monkeypatch, snapshot): def mock_query_items(container_proxy, query, **kwargs): - return MockCosmosDBResultsIterator(for_session_id_query) + return MockCosmosDBResultsIterator(for_message_pairs_query) monkeypatch.setattr(ContainerProxy, "query_items", mock_query_items) @@ -310,7 +332,7 @@ async def mock_read_item(container_proxy, item, partition_key, **kwargs): async def test_chathistory_deleteitem(auth_public_documents_client, monkeypatch): def mock_query_items(container_proxy, query, **kwargs): - return MockCosmosDBResultsIterator(for_session_id_query) + return MockCosmosDBResultsIterator(for_deletion_query) monkeypatch.setattr(ContainerProxy, "query_items", mock_query_items)