diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 06b5411b9..609632959 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -12,7 +12,7 @@ ARG USER_GID=$USER_UID # Set up non-root user COPY ./scripts/non-root-user.sh /tmp/ -RUN bash /tmp/non-root-user.sh "${USERNAME}" "${USER_UID}" "${USER_GID}" +RUN bash "/tmp/non-root-user.sh" "${USERNAME}" "${USER_UID}" "${USER_GID}" # Set env for tracking that we're running in a devcontainer ENV DEVCONTAINER=true diff --git a/.devcontainer/scripts/non-root-user.sh b/.devcontainer/scripts/non-root-user.sh index a4ad14172..2290f1f29 100755 --- a/.devcontainer/scripts/non-root-user.sh +++ b/.devcontainer/scripts/non-root-user.sh @@ -13,7 +13,6 @@ if [ "$(id -u)" -ne 0 ]; then exit 1 fi - # If in automatic mode, determine if a user already exists, if not use vscode if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then USERNAME="" @@ -39,11 +38,11 @@ fi # Create or update a non-root user to match UID/GID. if id -u ${USERNAME} > /dev/null 2>&1; then # User exists, update if needed - if [ "${USER_GID}" != "automatic" ] && [ "$USER_GID" != "$(id -G $USERNAME)" ]; then + if [ "${USER_GID}" != "automatic" ] && [ "${USER_GID}" != "999" ] && [ "$USER_GID" != "$(id -G $USERNAME)" ]; then groupmod --gid $USER_GID $USERNAME usermod --gid $USER_GID $USERNAME fi - if [ "${USER_UID}" != "automatic" ] && [ "$USER_UID" != "$(id -u $USERNAME)" ]; then + if [ "${USER_UID}" != "automatic" ] && [ "${USER_GID}" != "999" ] && [ "$USER_UID" != "$(id -u $USERNAME)" ]; then usermod --uid $USER_UID $USERNAME fi else diff --git a/.vscode/launch.json b/.vscode/launch.json index 5fad827a2..073c67e95 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -63,6 +63,17 @@ "request": "attach", "port": 9091, "preLaunchTask": "func host start" + }, + { + "name": "Vite: Debug", + "type": "msedge", + "request": "launch", + "url": "http://localhost:5000", + "webRoot": "${workspaceFolder}/app/backend/static", + "sourceMapPathOverrides": { + "webpack:///src/*": "${webRoot}/*" + }, + "skipFiles": ["/**", "**/node_modules/**"] } ] } \ No newline at end of file diff --git a/README.md b/README.md index c16961adf..09457a560 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,118 @@ # Information Assistant Accelerator -This accelerator demonstrates a few approaches for creating ChatGPT-like experiences over your own data using the Retrieval Augmented Generation pattern. It uses Azure OpenAI Service to access the ChatGPT model (gpt-35-turbo), and Azure Cognitive Search for data indexing and retrieval. +This industry accelerator showcases integration between Azure and OpenAI's large language models. It leverages Azure Cognitive Search for data retrieval and ChatGPT-style Q&A interactions. Using the Retrieval Augmented Generation (RAG) design pattern with Azure Open AI's GPT models, it provides a natural language interaction to discover relevant responses to user queries. Azure Cognitive Search simplifies data ingestion, transformation, indexing, and multilingual translation. + +The accelerator adapts prompts based on the model type for enhanced performance. Users can customize settings like temperature and persona for personalized AI interactions. It offers features like explainable thought processes, referenceable citations, and direct content for verification. --- -## Responsible AI -The Information Assistant (IA) Accelerator and Microsoft are committed to the advancement of AI driven by ethical principles that put people first. -**Read our [Transparency Note](./docs/transparency.md)** +![Process Flow](docs/process_flow.drawio.png) -Find out more with Microsoft's [Responsible AI resources](https://www.microsoft.com/en-us/ai/responsible-ai) +# Features ---- +## Retrieval Augmented Generation (RAG) -![Process Flow](docs/process_flow.drawio.png) +**Retrieve Contextually Relevant Documents:** Utilize Azure Cognitive Search's indexing capabilities to retrieve documents that are contextually relevant for precise answers. + +**Dynamic Model Selection:** Use GPT models (GPT-3.5 or GPT-4) tailored to your needs. + +Technical overview of RAG: [Retrieval Augmented Generation using Azure Machine Learning prompt flow](https://learn.microsoft.com/en-us/azure/machine-learning/concept-retrieval-augmented-generation?view=azureml-api-2#why-use-rag) + +## Prompt Engineering + +**Adaptable Prompt Structure:** Our prompt structure is designed to be compatible with current and future Azure OpenAI's Chat Completion API versions and GPT models, ensuring flexibility and sustainability. + +**Dynamic Prompts:** Dynamic prompt context based on the selected GPT model and users settings. + +**Built-in Chain of Thought (COT):** COT is integrated into our prompts to address fabrications that may arise with large language models (LLM). COT encourages the LLM to follow a set of instructions, explain its reasoning, and enhances the reliability of responses. + +**Few-Shot Prompting:** We employ few-shot prompting in conjunction with COT to further mitigate fabrications and improve response accuracy. + +Go here for more information on [Prompt engineering techniques](https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/advanced-prompt-engineering?pivots=programming-language-chat-completions) + +## Document Pre-Processing + +**Custom Document Chunking:** The Azure OpenAI GPT models have a maximum token limit, which includes both input and output tokens. Tokens are units of text which can represent a single word, a part of a word, or even a character, depending on the specific language and text encoding being used. Consequently the model will not be able to process a 500 page text based document. Likewise, the models will not be able to process complex file types, such as PDF. This is why we pre-process these documents, before passing these to our search capability to then be exposed by the RAG pattern. Our process focused on -## Features +* content extraction from text-based documents +* creating a standard JSON representation of all a documents text-based content +* chunking and saving metadata into manageable sized to be used in the RAG pattern -* Chat and Q&A interfaces -* File Upload and automated chunking and indexing for PDF, HTML, and DOCX -* Monitoring the status of files uploaded and processed by the accelerator -* Interacting with your data in supported native languages* -* Explores various options to help users evaluate the trustworthiness of responses with citations, tracking of source content, etc. -* Shows possible approaches for data preparation, prompt construction, and orchestration of interaction between model (ChatGPT) and retriever (Cognitive Search) -* Settings directly in the UX to tweak the behavior and experiment with options +Additional information on this process can be found [here](/docs/functions_flow.md) + +### Azure Cognitive Search Integration + +Search is used to index the chunks that were created during pre-processing. When a question is asked and an optimal search term is generated, this is passed to Search to identify and return the optimal set of chunks to be used in generation of the response. Some further details are listed below + +- **Data Enrichments:** Uses many Out-of-the-box Skillsets to extract enrichments from files such as utilizing Optical Character Recognition (OCR) to process images or converting tables within text into searchable text. + +- **Multilingual Translation:** Leverages the Text Translation skill to interact with your data in supported native languages*, expanding your application's global reach. *\*See [Configuring your own language ENV file](/docs/features/configuring_language_env_files.md) for supported languages* -![Chat screen](docs/images/chatscreen.png) +## Customization and Personalization + +**User-Selectable Options:** Users can fine-tune their interactions by adjusting settings such as temperature and persona, tailoring the AI experience to their specific needs. + +**UX Settings:** Easily tweak behavior and experiment with various options directly in the user interface. + +## Enhanced AI Interaction + +**Simple File Upload and Status:** We have put uploading of files into the Accelerator in the hands of the users by providing a simple drag-and-drop user interface for adding new content and a status page for monitoring document pre-processing. + +**Visualizing Thought Process:** Gain insights into the AI's decision-making process by visualizing how it arrives at answers, providing transparency and control. + +**Proper Citations and References:** The platform generates referenceable source content, designed to enhance trustworthiness and accountability in AI-generated responses. + +## Works in Progress (Future releases) + +**Incorporating Vector and Hybrid Search in Azure Cognitive Search:** We're actively working on enhancing Azure Cognitive Search by incorporating vector and hybrid search capabilities. This will enable more advanced search and retrieval mechanisms, further improving the precision and efficiency of document retrieval. + +**Adding Evaluation Guidance and Metrics:** To ensure transparency and accountability, we are researching comprehensive evaluation guidance and metrics. This will assist users in assessing the performance and trustworthiness of AI-generated responses, fostering confidence in the platform. + +**Research of [Unstructured.io](https://unstructured-io.github.io/unstructured/):** +The unstructured library is open source and designed to help pre-process unstructured data, such as documents, for use in downstream machine learning tasks. Our current position is we will continue with the Document Intelligence service, formerly Form Recognizer, for PDF pre-processing, but we will introduce unstructured.io as a catcher for many document types which we don't currently process. + +![Chat screen](docs/images/info_assistant_chatscreen.png) For a detailed review see our [Features](/docs/features/features.md) page. --- -# Getting Started + +## Data Collection Notice + +The software may collect information about you and your use of the software and send it to Microsoft. Microsoft may use this information to provide services and improve our products and services. You may turn off the telemetry as described in the repository. There are also some features in the software that may enable you and Microsoft to collect data from users of your applications. If you use these features, you must comply with applicable law, including providing appropriate notices to users of your applications together with a copy of Microsoft’s privacy statement. Our privacy statement is located at https://go.microsoft.com/fwlink/?LinkID=824704. You can learn more about data collection and use in the help documentation and our privacy statement. Your use of the software operates as your consent to these practices. + +### About Data Collection + +Data collection by the software in this repository is used by Microsoft solely to help justify the efforts of the teams who build and maintain this accelerator for our customers. It is your choice to leave this enabled, or to disable data collection. + +Data collection is implemented by the presence of a tracking GUID in the environment variables at deployment time. The GUID is associated with each Azure resource deployed by the installation scripts. This GUID is used by Microsoft to track the Azure consumption this open source solution generates. + +### How to Disable Data Collection + +To disable data collection, follow the instructions in the [Configure ENV files](/docs/development_environment.md#configure-env-files) section for `ENABLE_CUSTOMER_USAGE_ATTRIBUTION` variable before deploying. + +--- + +## Responsible AI + +The Information Assistant (IA) Accelerator and Microsoft are committed to the advancement of AI driven by ethical principles that put people first. + +**Read our [Transparency Note](./docs/transparency.md)** + +Find out more with Microsoft's [Responsible AI resources](https://www.microsoft.com/en-us/ai/responsible-ai) + +--- + +## Getting Started The IA Accelerator relies on multiple Azure services and has certain prerequisites that need to be met before deployment. It's essential to procure these prerequisites prior to proceeding with the deployment instructions in this guide. --- + ## Prerequisites + To get started with the IA Accelerator you will need the following: > >* An azure subscription with access enabled for the Azure OpenAI service. @@ -49,7 +125,20 @@ You can sign up for an Azure subscription [here](https://azure.microsoft.com/en- Once you have your prerequisite items, please move on to the Deployment Configuration step. -**NOTICE:** This codebase relies on the Azure OpenAI Service which must be procured first separately, subject to any applicable license agreement. Access to this code does not grant you a license or right to use Azure OpenAI Service. +**NOTICE:** * This codebase relies on the Azure OpenAI Service which must be procured first separately, subject to any applicable license agreement. Access to this code does not grant you a license or right to use Azure OpenAI Service. + +The Information Assistant Accelerator requires access to one of the following Azure OpenAI models. + +Model Name | Supported Versions +---|--- +gpt-35-turbo | 0301, 0613 +**gpt-35-turbo-16k** | N/A +**gpt-4** | N/A +gpt-4-32k | N/A + +**Important:** It is recommended to use gpt-4 models to achieve the best results from the IA Accelerator. Access to gpt-4 requires approval which can be requested [here](https://aka.ms/oai/get-gpt4). If gpt-4 access is not available gpt-35-turbo-16k (0613) is recommended. + +--- ## Deployment Configuration @@ -59,13 +148,13 @@ The deployment process for the IA Accelerator, uses a concept of **Developing in Begin by setting up your own Codespace using our [Developing in a Codespaces](docs/developing_in_a_codespaces.md) documentation. -*If you want to configure your local deskop for development container, follow our [Configuring your System for Development Containers](/docs/configure_local_dev_environment.md) guide. More information can be found at [Developing inside a Container](https://code.visualstudio.com/docs/remote/containers).* +*If you want to configure your local desktop for development container, follow our [Configuring your System for Development Containers](/docs/configure_local_dev_environment.md) guide. More information can be found at [Developing inside a Container](https://code.visualstudio.com/docs/remote/containers).* Once you have the completed the setting up Codespaces, please move on to the Sizing Estimation step. --- -# Sizing Estimator +## Sizing Estimator The IA Accelerator needs to be sized appropriately based on your use case. Please review our [Sizing Estimator](./docs/costestimator.md) to help find the configuration that fits your needs. @@ -73,31 +162,56 @@ Once you have completed the Sizing Estimator, please move on to the Deployment s --- -# Deployment +## Deployment -There are several steps to deploying the IA Accelerator. The following checklist will guide you through configuring the IA Accelerator in your environments. Please follow the steps in the order they are provided as values from one step may be used in subsequent steps. +The following checklist will guide you through configuring the IA Accelerator in your azure subscription. Please follow the steps in the order they are provided as values from one step may be used in subsequent steps. ->1. Configure Local Development Environment +>1. Configure your deployment settings > * [Configuring your Development Environment](/docs/development_environment.md) >1. Configure Azure resources > * [Configure Azure resources](/infra/README.md) --- -# Using IA Accelerator for the first time +## Using IA Accelerator for the first time Now that you have successfully deployed the IA Accelerator, you are ready to use the accelerator to process some data. To use the IA Accelerator, you need to follow these steps: > 1. Prepare your data and upload it to Azure. -> - Your data must be in a specified format to be valid for processing. See our [supported document types in the Feature documentation](/docs/features/features.md#document-pre-processing). -> - Upload your data [via the data upload user interface](/docs/features/features.md#uploading-documents). +> +> * Your data must be in a specified format to be valid for processing. See our [supported document types in the Feature documentation](/docs/features/features.md#document-pre-processing). +> * Upload your data [via the data upload user interface](/docs/features/features.md#uploading-documents). > 2. Once uploaded the system will automatically process and make the document(s) available to you and other users of the deployment. > 3. Begin [having conversations with your data](/docs/features/features.md#having-a-conversation-with-your-data) by selecting the appropriate interaction method. For more detailed information review the [Features](/docs/features/features.md) section of the documentation. +--- + +## Navigating the Source Code + +This project has the following structure: + +File/Folder | Description +---|--- +.devcontainer/ | Dockerfile, devcontainer configuration, and supporting script to enable both CodeSpaces and local DevContainers. +app/backend/ | The middleware part of the IA website that contains the prompt engineering and provides an API layer for the client code to pass through when communicating with the various Azure services. This code is python based and hosted as a Flask app. +app/frontend/ | The User Experience layer of the IA website. This code is Typescript based and hosted as a Vite app and compiled using npm. +azure_search/ | The configuration of the Azure Search Index, Indexer, Skillsets, and Data Source that are applied in the deployment scripts. +docs/adoption_workshop/ | PPT files that match what is covered in the Adoption Workshop videos in Discussions. +docs/features/ | Detailed documentation of specific features and development level configuration for Information Assistant. +docs/ | Deployment and other supporting documentation that is primarily linked to from the README.md +functions/ | The pipeline of Azure Functions that handle the document extraction and chunking as well as the custom CosmosDB logging. +infra/ | The BICEP scripts that deploy the entire IA Accelerator. The overall accelerator is orchestrated via the `main.bicep` file but most of the resource deployments are modularized under the **core** folder. +pipelines/ | Azure DevOps pipelines that can be used to enable CI/CD deployments of the accelerator. +scripts/environments/ | Deployment configuration files. This is where all external configuration values will be set. +scripts/ | Supporting scripts that perform the various deployment tasks such as infrastructure deployment, Azure WebApp and Function deployments, building of the webapp and functions source code, etc. These scripts align to the available commands in the `Makefile`. +Makefile | Deployment command definitions and configurations. You can use `make help` to get more details on available commands. +README.md | Starting point for this repo. It covers overviews of the Accelerator, Responsible AI, Environment, Deployment, and Usage of the Accelerator. + + --- ## Resources @@ -106,11 +220,14 @@ For more detailed information review the [Features](/docs/features/features.md) * [Azure Cognitive Search](https://learn.microsoft.com/azure/search/search-what-is-azure-search) * [Azure OpenAI Service](https://learn.microsoft.com/azure/cognitive-services/openai/overview) -# Trademarks +## Trademarks + This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow [Microsoft’s Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party’s policies. -# Code of Conduct +## Code of Conduct + This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. -# Reporting Security Issues -For security concerns, please see [Security Guidelines](./SECURITY.md) \ No newline at end of file +## Reporting Security Issues + +For security concerns, please see [Security Guidelines](./SECURITY.md) diff --git a/app/backend/app.py b/app/backend/app.py index e94538a2f..62e15a446 100644 --- a/app/backend/app.py +++ b/app/backend/app.py @@ -4,6 +4,7 @@ import logging import mimetypes import os +import json import urllib.parse from datetime import datetime, timedelta @@ -11,6 +12,7 @@ from approaches.chatreadretrieveread import ChatReadRetrieveReadApproach from azure.core.credentials import AzureKeyCredential from azure.identity import DefaultAzureCredential +from azure.mgmt.cognitiveservices import CognitiveServicesManagementClient from azure.search.documents import SearchClient from azure.storage.blob import ( AccountSasPermissions, @@ -33,10 +35,12 @@ AZURE_SEARCH_SERVICE_KEY = os.environ.get("AZURE_SEARCH_SERVICE_KEY") AZURE_SEARCH_INDEX = os.environ.get("AZURE_SEARCH_INDEX") or "gptkbindex" AZURE_OPENAI_SERVICE = os.environ.get("AZURE_OPENAI_SERVICE") or "myopenai" +AZURE_OPENAI_RESOURCE_GROUP = os.environ.get("AZURE_OPENAI_RESOURCE_GROUP") or "" AZURE_OPENAI_CHATGPT_DEPLOYMENT = ( os.environ.get("AZURE_OPENAI_CHATGPT_DEPLOYMENT") or "chat" ) AZURE_OPENAI_SERVICE_KEY = os.environ.get("AZURE_OPENAI_SERVICE_KEY") +AZURE_SUBSCRIPTION_ID = os.environ.get("AZURE_SUBSCRIPTION_ID") KB_FIELDS_CONTENT = os.environ.get("KB_FIELDS_CONTENT") or "merged_content" KB_FIELDS_CATEGORY = os.environ.get("KB_FIELDS_CATEGORY") or "category" @@ -83,6 +87,15 @@ ) blob_container = blob_client.get_container_client(AZURE_BLOB_STORAGE_CONTAINER) +# Set up OpenAI management client +openai_mgmt_client = CognitiveServicesManagementClient( + credential=azure_credential, + subscription_id=AZURE_SUBSCRIPTION_ID) + +deployment = openai_mgmt_client.deployments.get( + resource_group_name=AZURE_OPENAI_RESOURCE_GROUP, + account_name=AZURE_OPENAI_SERVICE, + deployment_name=AZURE_OPENAI_CHATGPT_DEPLOYMENT) chat_approaches = { "rrr": ChatReadRetrieveReadApproach( @@ -94,6 +107,8 @@ KB_FIELDS_CONTENT, blob_client, QUERY_TERM_LANGUAGE, + deployment.properties.model.name, + deployment.properties.model.version ) } @@ -105,33 +120,6 @@ def static_file(path): return app.send_static_file(path) - -# Return blob path with SAS token for citation access -@app.route("/content/") -def content_file(path): - blob = blob_container.get_blob_client(path).download_blob() - mime_type = blob.properties["content_settings"]["content_type"] - file_extension = blob.properties["name"].split(".")[-1:] - if mime_type == "application/octet-stream": - mime_type = mimetypes.guess_type(path)[0] or "application/octet-stream" - if mime_type == "text/plain" and file_extension[0] in ["htm", "html"]: - mime_type = "text/html" - print( - "Using mime type: " - + mime_type - + "for file with extension: " - + file_extension[0] - ) - return ( - blob.readall(), - 200, - { - "Content-Type": mime_type, - "Content-Disposition": f"inline; filename={urllib.parse.quote(path, safe='')}", - }, - ) - - @app.route("/chat", methods=["POST"]) def chat(): approach = request.json["approach"] @@ -156,7 +144,6 @@ def chat(): logging.exception("Exception in /chat") return jsonify({"error": str(e)}), 500 - @app.route("/getblobclienturl") def get_blob_client_url(): sas_token = generate_account_sas( @@ -177,11 +164,6 @@ def get_blob_client_url(): ) return jsonify({"url": f"{blob_client.url}?{sas_token}"}) - -if __name__ == "__main__": - app.run() - - @app.route("/getalluploadstatus", methods=["POST"]) def get_all_upload_status(): timeframe = request.json["timeframe"] @@ -192,3 +174,33 @@ def get_all_upload_status(): logging.exception("Exception in /getalluploadstatus") return jsonify({"error": str(e)}), 500 return jsonify(results) + +# Return AZURE_OPENAI_CHATGPT_DEPLOYMENT +@app.route("/getInfoData") +def get_info_data(): + response = jsonify( + { + "AZURE_OPENAI_CHATGPT_DEPLOYMENT": f"{AZURE_OPENAI_CHATGPT_DEPLOYMENT}", + "AZURE_OPENAI_MODEL_NAME": f"{deployment.properties.model.name}", + "AZURE_OPENAI_MODEL_VERSION": f"{deployment.properties.model.version}", + "AZURE_OPENAI_SERVICE": f"{AZURE_OPENAI_SERVICE}", + "AZURE_SEARCH_SERVICE": f"{AZURE_SEARCH_SERVICE}", + "AZURE_SEARCH_INDEX": f"{AZURE_SEARCH_INDEX}", + "TARGET_LANGUAGE": f"{QUERY_TERM_LANGUAGE}" + }) + return response + +@app.route("/getcitation", methods=["POST"]) +def get_citation(): + citation = urllib.parse.unquote(request.json["citation"]) + try: + blob = blob_container.get_blob_client(citation).download_blob() + decoded_text = blob.readall().decode() + results = jsonify(json.loads(decoded_text)) + except Exception as e: + logging.exception("Exception in /getalluploadstatus") + return jsonify({"error": str(e)}), 500 + return jsonify(results.json) + +if __name__ == "__main__": + app.run() diff --git a/app/backend/approaches/chatreadretrieveread.py b/app/backend/approaches/chatreadretrieveread.py index 2bb813667..ae207e816 100644 --- a/app/backend/approaches/chatreadretrieveread.py +++ b/app/backend/approaches/chatreadretrieveread.py @@ -1,10 +1,8 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - import json import logging import urllib.parse from datetime import datetime, timedelta +from typing import Any, Sequence import openai from approaches.approach import Approach @@ -17,88 +15,69 @@ generate_account_sas, ) from text import nonewlines +import tiktoken +from core.messagebuilder import MessageBuilder +from core.modelhelper import get_token_limit +from core.modelhelper import num_tokens_from_messages +# Simple retrieve-then-read implementation, using the Cognitive Search and +# OpenAI APIs directly. It first retrieves top documents from search, +# then constructs a prompt with them, and then uses OpenAI to generate +# an completion (answer) with that prompt. class ChatReadRetrieveReadApproach(Approach): - """ - Simple retrieve-then-read implementation, using the Cognitive Search and OpenAI APIs directly. It first retrieves - top documents from search, then constructs a prompt with them, and then uses OpenAI to generate an completion - (answer) with that prompt. - """ - - prompt_prefix = """<|im_start|> - You are an Azure OpenAI Completion system. Your persona is {systemPersona} who helps answer questions about an agency's data. {response_length_prompt} - - - Text: - Flight to Denver at 9:00 am tomorrow. - - Prompt: - Question: Is my flight on time? - - Steps: - 1. Look for relevant information in the provided source document to answer the question. - 2. If there is specific flight information available in the source document, provide an answer along with the appropriate citation. - 3. If there is no information about the specific flight in the source document, respond with "I'm not sure" without providing any citation. - - - Response: - 1. Look for relevant information in the provided source document to answer the question. - - Search for flight details matching the given flight to determine its current status. - - 2. If there is specific flight information available in the source document, provide an answer along with the appropriate citation. - - If the source document contains information about the current status of the specified flight, provide a response citing the relevant section of source documents.Don't exclude citation if you are using source document to answer your question. + # Chat roles + SYSTEM = "system" + USER = "user" + ASSISTANT = "assistant" + system_message_chat_conversation = """You are an Azure OpenAI Completion system. Your persona is {systemPersona} who helps answer questions about an agency's data. {response_length_prompt} + User persona is {userPersona} Answer ONLY with the facts listed in the list of sources above. + Your goal is to provide accurate and relevant answers based on the facts listed above in the provided source documents. Make sure to reference the above source documents appropriately and avoid making assumptions or adding personal opinions. - 3. If there is no relevant information about the specific flight in the source document, respond with "I'm not sure" without providing any citation. + Emphasize the use of facts listed in the above provided source documents.Instruct the model to use source name for each fact used in the response. Avoid generating speculative or generalized information. Each source has a file name followed by a pipe character and + the actual information.Use square brackets to reference the source, e.g. [info1.txt]. Do not combine sources, list each source separately, e.g. [info1.txt][info2.pdf]. - - Example Response: - - Question: Is my flight on time? - - I'm not sure. The provided source document does not include information about the current status of your specific flight. + Here is how you should answer every question: + + -Look for relevant information in the above source documents to answer the question. + -If the source document does not include the exact answer, please respond with relevant information from the data in the response along with citation.You must include a citation to each document referenced. + -If you cannot find any relevant information in the above sources, respond with I am not sure.Do not provide personal opinions or assumptions. - - User persona: {userPersona} - Emphasize the use of facts listed in the provided source documents.Instruct the model to use source name for each fact used in the response. Avoid generating speculative or generalized information. Each source has a file name followed by a pipe character and - the actual information.Use square brackets to reference the source, e.g. [info1.txt]. Don't combine sources, list each source separately, e.g. [info1.txt][info2.pdf]. - Treat each search term as an individual keyword. Do not combine terms in quotes or brackets. - Your goal is to provide accurate and relevant answers based on the information available in the provided source documents. Make sure to reference the source documents appropriately and avoid making assumptions or adding personal opinions. - - {follow_up_questions_prompt} {injected_prompt} - Sources: - {sources} - - <|im_end|> - {chat_history} """ follow_up_questions_prompt_content = """ Generate three very brief follow-up questions that the user would likely ask next about their agencies data. Use triple angle brackets to reference the questions, e.g. <<>>. Try not to repeat questions that have already been asked. Only generate questions and do not generate any text before or after the questions, such as 'Next Questions' """ - - query_prompt_template = """ - Below is a history of the conversation so far, and a new question asked by the user that needs to be answered by searching in source documents - Generate a search query based on the conversation and the new question. + query_prompt_template = """Below is a history of the conversation so far, and a new question asked by the user that needs to be answered by searching in source documents. + Generate a search query based on the conversation and the new question. Treat each search term as an individual keyword. Do not combine terms in quotes or brackets. Do not include cited source filenames and document names e.g info.txt or doc.pdf in the search query terms. Do not include any text inside [] or <<<>>> in the search query terms. + Do not include any special characters like '+'. If the question is not in {query_term_language}, translate the question to {query_term_language} before generating the search query. - Treat each search term as an individual keyword. Do not combine terms in quotes or brackets. - - Chat History: - {chat_history} - - Question: - {question} - - Search query: + If you cannot generate a search query, return just the number 0. """ + #Few Shot prompting for Keyword Search Query + query_prompt_few_shots = [ + {'role' : USER, 'content' : 'What are the future plans for public transportation development?' }, + {'role' : ASSISTANT, 'content' : 'Future plans for public transportation' }, + {'role' : USER, 'content' : 'how much renewable energy was generated last year?' }, + {'role' : ASSISTANT, 'content' : 'Renewable energy generation last year' } + ] + + #Few Shot prompting for Response. This will feed into Chain of thought system message. + response_prompt_few_shots = [ + {"role": USER ,'content': 'I am looking for information in source documents'}, + {'role': ASSISTANT, 'content': 'user is looking for information in source documents. Do not provide answers that are not in the source documents'}, + {'role': USER, 'content': 'What steps are being taken to promote energy conservation?'}, + {'role': ASSISTANT, 'content': 'Several steps are being taken to promote energy conservation including reducing energy consumption, increasing energy efficiency, and increasing the use of renewable energy sources.Citations[info1.json]'} + ] + def __init__( self, search_client: SearchClient, @@ -109,6 +88,8 @@ def __init__( content_field: str, blob_client: BlobServiceClient, query_term_language: str, + model_name: str, + model_version: str ): self.search_client = search_client self.chatgpt_deployment = chatgpt_deployment @@ -116,60 +97,53 @@ def __init__( self.content_field = content_field self.blob_client = blob_client self.query_term_language = query_term_language + self.chatgpt_token_limit = get_token_limit(chatgpt_deployment) - openai.api_base = "https://" + oai_service_name + ".openai.azure.com/" - openai.api_type = "azure" + openai.api_base = 'https://' + oai_service_name + '.openai.azure.com/' + openai.api_type = 'azure' openai.api_key = oai_service_key - def run(self, history: list[dict], overrides: dict) -> any: - """ - Run the approach on the query and documents. + self.model_name = model_name + self.model_version = model_version - Args: - history: The chat history. (e.g. [{"user": "hello", "bot": "hi"}]) - overrides: Overrides from the user interface for the approach. (e.g. temperature, top, - semantic_captions etc.) - - Returns: - The response from the approach as a dict. - """ - # Overrides + # def run(self, history: list[dict], overrides: dict) -> any: + def run(self, history: Sequence[dict[str, str]], overrides: dict[str, Any]) -> Any: use_semantic_captions = True if overrides.get("semantic_captions") else False top = overrides.get("top") or 3 - response_length = int(overrides.get("response_length") or 1024) - - ## Category filter exclude_category = overrides.get("exclude_category") or None - category_filter = ( - "category ne '{}'".format(exclude_category.replace("'", "''")) - if exclude_category - else None - ) - - ## Personas + category_filter = "category ne '{}'".format(exclude_category.replace("'", "''")) if exclude_category else None user_persona = overrides.get("user_persona", "") system_persona = overrides.get("system_persona", "") + response_length = int(overrides.get("response_length") or 1024) - # STEP 1: Generate an optimized keyword search query based on the chat history and the last question. + user_q = 'Generate search query for: ' + history[-1]["user"] - prompt = self.query_prompt_template.format( - chat_history=self.get_chat_history_as_text( - history, include_last_turn=False - ), - question=history[-1]["user"], - query_term_language=self.query_term_language, - ) + query_prompt=self.query_prompt_template.format(query_term_language=self.query_term_language) + + # STEP 1: Generate an optimized keyword search query based on the chat history and the last question + messages = self.get_messages_from_history( + query_prompt, + self.chatgpt_deployment, + history, + user_q, + self.query_prompt_few_shots, + self.chatgpt_token_limit - len(user_q) + ) - completion = openai.Completion.create( - engine=self.chatgpt_deployment, - prompt=prompt, + chat_completion = openai.ChatCompletion.create( + + deployment_id=self.chatgpt_deployment, + model=self.chatgpt_deployment, + messages=messages, temperature=0.0, max_tokens=32, - n=1, - stop=["\n"], - ) - raw_query_proposal = completion.choices[0].text - generated_query = raw_query_proposal.strip('"') + n=1) + + generated_query = chat_completion.choices[0].message.content + + #if we fail to generate a query, return the last user question + if generated_query.strip() == "0": + generated_query = history[-1]["user"] # STEP 2: Retrieve relevant documents from the search index with the optimized query term if overrides.get("semantic_ranker"): @@ -223,6 +197,8 @@ def run(self, history: list[dict], overrides: dict) -> any: + "| " + nonewlines(doc[self.content_field]) ) + # uncomment to debug size of each search result content_field + print(f"File{idx}: ", self.num_tokens_from_string(f"File{idx} " + "| " + nonewlines(doc[self.content_field]), "cl100k_base")) # add the "FileX" moniker and full file name to the citation lookup citation_lookup[f"File{idx}"] = { @@ -232,6 +208,10 @@ def run(self, history: list[dict], overrides: dict) -> any: doc[self.content_field] ), } + + + + # create a single string of all the results to be used in the prompt results_text = "".join(results) @@ -249,11 +229,10 @@ def run(self, history: list[dict], overrides: dict) -> any: # Allow client to replace the entire prompt, or to inject into the existing prompt using >>> prompt_override = overrides.get("prompt_template") + if prompt_override is None: - prompt = self.prompt_prefix.format( + system_message = self.system_message_chat_conversation.format( injected_prompt="", - sources=content, - chat_history=self.get_chat_history_as_text(history), follow_up_questions_prompt=follow_up_questions_prompt, response_length_prompt=self.get_response_length_prompt_text( response_length @@ -262,10 +241,8 @@ def run(self, history: list[dict], overrides: dict) -> any: systemPersona=system_persona, ) elif prompt_override.startswith(">>>"): - prompt = self.prompt_prefix.format( + system_message = self.system_message_chat_conversation.format( injected_prompt=prompt_override[3:] + "\n ", - sources=content, - chat_history=self.get_chat_history_as_text(history), follow_up_questions_prompt=follow_up_questions_prompt, response_length_prompt=self.get_response_length_prompt_text( response_length @@ -274,9 +251,7 @@ def run(self, history: list[dict], overrides: dict) -> any: systemPersona=system_persona, ) else: - prompt = prompt_override.format( - sources=content, - chat_history=self.get_chat_history_as_text(history), + system_message = self.system_message_chat_conversation.format( follow_up_questions_prompt=follow_up_questions_prompt, response_length_prompt=self.get_response_length_prompt_text( response_length @@ -284,65 +259,131 @@ def run(self, history: list[dict], overrides: dict) -> any: userPersona=user_persona, systemPersona=system_persona, ) + # STEP 3: Generate a contextual and content-specific answer using the search results and chat history. + #Added conditional block to use different system messages for different models. + + if self.model_name.startswith("gpt-35-turbo"): + messages = self.get_messages_from_history( + system_message, + self.chatgpt_deployment, + history, + history[-1]["user"] + "Sources:\n" + content + "\n\n", + self.response_prompt_few_shots, + max_tokens=self.chatgpt_token_limit - 500 + ) - # STEP 3: Generate a contextual and content-specific answer using the search results and chat history + #Uncomment to debug token usage. + #print(messages) + #message_string = "" + #for message in messages: + # # enumerate the messages and add the role and content elements of the dictoinary to the message_string + # message_string += f"{message['role']}: {message['content']}\n" + #print("Content Tokens: ", self.num_tokens_from_string("Sources:\n" + content + "\n\n", "cl100k_base")) + #print("System Message Tokens: ", self.num_tokens_from_string(system_message, "cl100k_base")) + #print("Few Shot Tokens: ", self.num_tokens_from_string(self.response_prompt_few_shots[0]['content'], "cl100k_base")) + #print("Message Tokens: ", self.num_tokens_from_string(message_string, "cl100k_base")) + + + chat_completion = openai.ChatCompletion.create( + deployment_id=self.chatgpt_deployment, + model=self.chatgpt_deployment, + messages=messages, + temperature=float(overrides.get("response_temp")) or 0.6, + n=1 + ) + + elif self.model_name.startswith("gpt-4"): + messages = self.get_messages_from_history( + "Sources:\n" + content + "\n\n" + system_message, + # system_message + "\n\nSources:\n" + content, + self.chatgpt_deployment, + history, + history[-1]["user"], + self.response_prompt_few_shots, + max_tokens=self.chatgpt_token_limit + ) + + #Uncomment to debug token usage. + #print(messages) + #message_string = "" + #for message in messages: + # # enumerate the messages and add the role and content elements of the dictoinary to the message_string + # message_string += f"{message['role']}: {message['content']}\n" + #print("Content Tokens: ", self.num_tokens_from_string("Sources:\n" + content + "\n\n", "cl100k_base")) + #print("System Message Tokens: ", self.num_tokens_from_string(system_message, "cl100k_base")) + #print("Few Shot Tokens: ", self.num_tokens_from_string(self.response_prompt_few_shots[0]['content'], "cl100k_base")) + #print("Message Tokens: ", self.num_tokens_from_string(message_string, "cl100k_base")) + + chat_completion = openai.ChatCompletion.create( + deployment_id=self.chatgpt_deployment, + model=self.chatgpt_deployment, + messages=messages, + temperature=float(overrides.get("response_temp")) or 0.6, + max_tokens=1024, + n=1 - completion = openai.Completion.create( - engine=self.chatgpt_deployment, - prompt=prompt, - temperature=float(overrides.get("response_temp")) or 0.7, - max_tokens=response_length, - n=1, - stop=["<|im_end|>", "<|im_start|>"], ) + # chat_completion = openai.ChatCompletion.create( + # deployment_id=self.chatgpt_deployment, + # model=self.chatgpt_deployment, + # messages=messages, + # temperature=float(overrides.get("response_temp")) or 0.6, + # max_tokens=1024, + # n=1 + + # ) + + #Aparmar.Token Debugging Code. Uncomment to debug token usage. + # generated_response_message = chat_completion.choices[0].message + # # Count the tokens in the generated response message + # token_count = num_tokens_from_messages(generated_response_message, 'gpt-4') + # print("Generated Response Tokens:", token_count) + + msg_to_display = '\n\n'.join([str(message) for message in messages]) + return { "data_points": data_points, - "answer": f"{urllib.parse.unquote(completion.choices[0].text)}", - "thoughts": f"Searched for:
{generated_query}

Prompt:
" - + prompt.replace("\n", "
"), - "citation_lookup": citation_lookup, + "answer": f"{urllib.parse.unquote(chat_completion.choices[0].message.content)}", + "thoughts": f"Searched for:
{generated_query}

Conversations:
" + msg_to_display.replace('\n', '
'), + "citation_lookup": citation_lookup } - def get_chat_history_as_text(self, history, include_last_turn=True) -> str: + #Aparmar. Custom method to construct Chat History as opposed to single string of chat History. + def get_messages_from_history( + self, + system_prompt: str, + model_id: str, + history: Sequence[dict[str, str]], + user_conv: str, + few_shots = [], + max_tokens: int = 4096) -> []: + """ + Construct a list of messages from the chat history and the user's question. """ - Get the chat history as a single string of text for presenting to the user. + message_builder = MessageBuilder(system_prompt, model_id) - Args: - history: The chat history. (e.g. [{"user": "hello", "bot": "hi"}]) - include_last_turn: Whether to include the last turn in the chat history. + # Few Shot prompting. Add examples to show the chat what responses we want. It will try to mimic any responses and make sure they match the rules laid out in the system message. + for shot in few_shots: + message_builder.append_message(shot.get('role'), shot.get('content')) - Returns: - The chat history as a single string of text. - """ - history_text = "" - for h in reversed(history if include_last_turn else history[:-1]): - history_text = ( - """User:""" - + " " - + h["user"] - + "\n" - + """""" - + "\n" - + """Assistant:""" - + " " - + (h.get("bot") + """""" if h.get("bot") else "") - + "\n" - + history_text - ) + user_content = user_conv + append_index = len(few_shots) + 1 - return history_text + message_builder.append_message(self.USER, user_content, index=append_index) - def get_response_length_prompt_text(self, response_length: int): - """ - Get the prompt text for the response length + for h in reversed(history[:-1]): + if h.get("bot"): + message_builder.append_message(self.ASSISTANT, h.get('bot'), index=append_index) + message_builder.append_message(self.USER, h.get('user'), index=append_index) + if message_builder.token_length > max_tokens: + break - Args: - response_length: The response length mapped to a prompt text. + messages = message_builder.messages + return messages - Returns: - The prompt text for the response length. - """ + #Get the prompt text for the response length + def get_response_length_prompt_text(self, response_length: int): levels = { 1024: "succinct", 2048: "standard", @@ -402,3 +443,9 @@ def get_first_page_num_for_chunk(self, content: str) -> str: except Exception as error: logging.exception("Unable to parse first page num: " + str(error) + "") return "0" + + def num_tokens_from_string(self, string: str, encoding_name: str) -> int: + """ Function to return the number of tokens in a text string""" + encoding = tiktoken.get_encoding(encoding_name) + num_tokens = len(encoding.encode(string)) + return num_tokens \ No newline at end of file diff --git a/app/backend/core/messagebuilder.py b/app/backend/core/messagebuilder.py new file mode 100644 index 000000000..3c3980ca2 --- /dev/null +++ b/app/backend/core/messagebuilder.py @@ -0,0 +1,25 @@ +from .modelhelper import num_tokens_from_messages + + +class MessageBuilder: + """ + A class for building and managing messages in a chat conversation. + Attributes: + message (list): A list of dictionaries representing chat messages. + model (str): The name of the ChatGPT model. + token_count (int): The total number of tokens in the conversation. + Methods: + __init__(self, system_content: str, chatgpt_model: str): Initializes the MessageBuilder instance. + append_message(self, role: str, content: str, index: int = 1): Appends a new message to the conversation. + """ + + def __init__(self, system_content: str, chatgpt_model: str): + self.messages = [{'role': 'system', 'content': system_content}] + self.model = chatgpt_model + self.token_length = num_tokens_from_messages( + self.messages[-1], self.model) + + def append_message(self, role: str, content: str, index: int = 1): + self.messages.insert(index, {'role': role, 'content': content}) + self.token_length += num_tokens_from_messages( + self.messages[index], self.model) \ No newline at end of file diff --git a/app/backend/core/modelhelper.py b/app/backend/core/modelhelper.py new file mode 100644 index 000000000..6508416e5 --- /dev/null +++ b/app/backend/core/modelhelper.py @@ -0,0 +1,51 @@ +import tiktoken + +MODELS_2_TOKEN_LIMITS = { + "gpt-35-turbo": 4000, + "gpt-3.5-turbo": 4000, + "gpt-35-turbo-16k": 16000, + "gpt-3.5-turbo-16k": 16000, + "gpt-4": 8100, + "gpt-4-32k": 32000 +} + +AOAI_2_OAI = { + "gpt-35-turbo": "gpt-3.5-turbo", + "gpt-35-turbo-16k": "gpt-3.5-turbo-16k" +} + + +def get_token_limit(model_id: str) -> int: + if model_id not in MODELS_2_TOKEN_LIMITS: + raise ValueError("Expected model gpt-35-turbo and above") + return MODELS_2_TOKEN_LIMITS.get(model_id) + + +def num_tokens_from_messages(message: dict[str, str], model: str) -> int: + """ + Calculate the number of tokens required to encode a message. + Args: + message (dict): The message to encode, represented as a dictionary. + model (str): The name of the model to use for encoding. + Returns: + int: The total number of tokens required to encode the message. + Example: + message = {'role': 'user', 'content': 'Hello, how are you?'} + model = 'gpt-3.5-turbo' + num_tokens_from_messages(message, model) + output: 11 + """ + encoding = tiktoken.encoding_for_model(get_oai_chatmodel_tiktok(model)) + num_tokens = 2 # For "role" and "content" keys + for key, value in message.items(): + num_tokens += len(encoding.encode(value)) + return num_tokens + + +def get_oai_chatmodel_tiktok(aoaimodel: str) -> str: + message = "Expected Azure OpenAI ChatGPT model name" + if aoaimodel == "" or aoaimodel is None: + raise ValueError(message) + if aoaimodel not in AOAI_2_OAI and aoaimodel not in MODELS_2_TOKEN_LIMITS: + raise ValueError(message) + return AOAI_2_OAI.get(aoaimodel) or aoaimodel \ No newline at end of file diff --git a/app/backend/requirements.txt b/app/backend/requirements.txt index c72111f1f..2ff021a29 100644 --- a/app/backend/requirements.txt +++ b/app/backend/requirements.txt @@ -1,7 +1,9 @@ azure-identity==1.12.0 Flask==2.3.2 langchain>=0.0.157 -openai==0.26.4 +azure-mgmt-cognitiveservices==13.5.0 +openai==0.27.0 azure-search-documents==11.4.0b3 azure-storage-blob==12.14.1 -azure-cosmos == 4.3.1 \ No newline at end of file +azure-cosmos == 4.3.1 +tiktoken == 0.4.0 \ No newline at end of file diff --git a/app/frontend/src/api/api.ts b/app/frontend/src/api/api.ts index e21857f8e..c48bb17cc 100644 --- a/app/frontend/src/api/api.ts +++ b/app/frontend/src/api/api.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { AskRequest, AskResponse, ChatRequest, BlobClientUrlResponse, AllFilesUploadStatus, GetUploadStatusRequest } from "./models"; +import { AskRequest, AskResponse, ChatRequest, BlobClientUrlResponse, AllFilesUploadStatus, GetUploadStatusRequest, GetInfoResponse, ActiveCitation } from "./models"; export async function askApi(options: AskRequest): Promise { const response = await fetch("/ask", { @@ -73,7 +73,7 @@ export async function chatApi(options: ChatRequest): Promise { } export function getCitationFilePath(citation: string): string { - return `/content/${encodeURIComponent(citation)}`; + return `${encodeURIComponent(citation)}`; } export async function getBlobClientUrl(): Promise { @@ -110,4 +110,38 @@ export async function getAllUploadStatus(options: GetUploadStatusRequest): Promi } const results: AllFilesUploadStatus = {statuses: parsedResponse}; return results; +} + +export async function getInfoData(): Promise { + const response = await fetch("/getInfoData", { + method: "GET", + headers: { + "Content-Type": "application/json" + } + }); + const parsedResponse: GetInfoResponse = await response.json(); + if (response.status > 299 || !response.ok) { + console.log(response); + throw Error(parsedResponse.error || "Unknown error"); + } + console.log(parsedResponse); + return parsedResponse; +} + +export async function getCitationObj(citation: string): Promise { + const response = await fetch(`/getcitation`, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + citation: citation + }) + }); + const parsedResponse: ActiveCitation = await response.json(); + if (response.status > 299 || !response.ok) { + console.log(response); + throw Error(parsedResponse.error || "Unknown error"); + } + return parsedResponse; } \ No newline at end of file diff --git a/app/frontend/src/api/models.ts b/app/frontend/src/api/models.ts index 6c65fd997..59e291099 100644 --- a/app/frontend/src/api/models.ts +++ b/app/frontend/src/api/models.ts @@ -76,6 +76,7 @@ export type GetUploadStatusRequest = { state: FileState } + // These keys need to match case with the defined Enum in the // shared code (functions/shared_code/status_log.py) export const enum FileState { @@ -86,3 +87,27 @@ export const enum FileState { Complete = "COMPLETE", Error = "ERROR" } + + +export type GetInfoResponse = { + AZURE_OPENAI_SERVICE: string; + AZURE_OPENAI_CHATGPT_DEPLOYMENT: string; + AZURE_OPENAI_MODEL_NAME: string; + AZURE_OPENAI_MODEL_VERSION: string; + AZURE_SEARCH_SERVICE: string; + AZURE_SEARCH_INDEX: string; + TARGET_LANGUAGE: string; + error?: string; +}; + +export type ActiveCitation = { + file_name: string; + file_uri: string; + processed_datetime: string; + title: string; + section: string; + pages: number[]; + token_count: number; + content: string; + error?: string; +} \ No newline at end of file diff --git a/app/frontend/src/components/AnalysisPanel/AnalysisPanel.tsx b/app/frontend/src/components/AnalysisPanel/AnalysisPanel.tsx index 75ca93f24..9e71a0400 100644 --- a/app/frontend/src/components/AnalysisPanel/AnalysisPanel.tsx +++ b/app/frontend/src/components/AnalysisPanel/AnalysisPanel.tsx @@ -1,13 +1,16 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { Pivot, PivotItem } from "@fluentui/react"; +import { useEffect, useState } from "react"; +import { Pivot, PivotItem, Text } from "@fluentui/react"; +import { Label } from '@fluentui/react/lib/Label'; +import { Separator } from '@fluentui/react/lib/Separator'; import DOMPurify from "dompurify"; import styles from "./AnalysisPanel.module.css"; import { SupportingContent } from "../SupportingContent"; -import { AskResponse } from "../../api"; +import { AskResponse, ActiveCitation, getCitationObj } from "../../api"; import { AnalysisPanelTabs } from "./AnalysisPanelTabs"; interface Props { @@ -24,15 +27,29 @@ interface Props { const pivotItemDisabledStyle = { disabled: true, style: { color: "grey" } }; export const AnalysisPanel = ({ answer, activeTab, activeCitation, sourceFile, pageNumber, citationHeight, className, onActiveTabChanged }: Props) => { + const [activeCitationObj, setActiveCitationObj] = useState(); + const isDisabledThoughtProcessTab: boolean = !answer.thoughts; const isDisabledSupportingContentTab: boolean = !answer.data_points.length; const isDisabledCitationTab: boolean = !activeCitation; // the first split on ? separates the file from the sas token, then the second split on . separates the file extension const sourceFileExt: any = sourceFile?.split("?")[0].split(".").pop(); - const sanitizedThoughts = DOMPurify.sanitize(answer.thoughts!); - - console.log(sourceFile?.split("?")[0].split(".").pop()) + + async function fetchActiveCitationObj() { + try { + const citationObj = await getCitationObj(activeCitation as string); + setActiveCitationObj(citationObj); + console.log(citationObj); + } catch (error) { + // Handle the error here + console.log(error); + } + } + + useEffect(() => { + fetchActiveCitationObj(); + }, [activeCitation]); return ( -