Copilot user db (#1672)

* Implement copilot for user database

* Fix minor bugs

* fix bugs

* Add user database copilot

* Add placeholder text on copilot

* Add AFEC adn killswitch

* Add new v2 sampledatabase endpoint

* Add telemetry

* fix telemetry bug

* Add query edited telemetry

* add authorization header

* Add back to the staging env for phoenix

* point to stage for phoenix

* Preview commit for test env

* Preview link for staging

* change the staging url

* fix lint, unit tests

* fix lint, unit tests

* fix formatting
This commit is contained in:
sunghyunkang1111
2023-11-09 11:55:25 -06:00
committed by GitHub
parent a5e1b37ba6
commit 0e124f4881
38 changed files with 911 additions and 252 deletions

View File

@@ -1,10 +1,10 @@
import { Checkbox, ChoiceGroup, DefaultButton, IconButton, PrimaryButton, TextField } from "@fluentui/react";
import Explorer from "Explorer/Explorer";
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { getUserEmail } from "Utils/UserUtils";
import { shallow } from "enzyme";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
jest.mock("Utils/UserUtils");
@@ -13,21 +13,49 @@ jest.mock("Utils/UserUtils");
jest.mock("Explorer/QueryCopilot/Shared/QueryCopilotClient");
SubmitFeedback as jest.Mock;
jest.mock("Explorer/QueryCopilot/QueryCopilotContext");
const mockUseCopilotStore = useCopilotStore as jest.Mock;
const mockReturnValue = {
generatedQuery: "test query",
userPrompt: "test prompt",
likeQuery: false,
showFeedbackModal: false,
closeFeedbackModal: jest.fn,
setHideFeedbackModalForLikedQueries: jest.fn,
};
describe("Query Copilot Feedback Modal snapshot test", () => {
beforeEach(() => {
mockUseCopilotStore.mockReturnValue(mockReturnValue);
jest.clearAllMocks();
});
it("shoud render and match snapshot", () => {
useQueryCopilot.getState().openFeedbackModal("test query", false, "test prompt");
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
mockUseCopilotStore.mockReturnValue({
...mockReturnValue,
showFeedbackModal: true,
});
const wrapper = shallow(
<QueryCopilotFeedbackModal
explorer={new Explorer()}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
expect(wrapper.props().isOpen).toBeTruthy();
expect(wrapper).toMatchSnapshot();
});
it("should close on cancel click", () => {
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
const wrapper = shallow(
<QueryCopilotFeedbackModal
explorer={new Explorer()}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
const cancelButton = wrapper.find(IconButton);
cancelButton.simulate("click");
@@ -38,7 +66,14 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
});
it("should get user unput", () => {
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
const wrapper = shallow(
<QueryCopilotFeedbackModal
explorer={new Explorer()}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
const testUserInput = "test user input";
const userInput = wrapper.find(TextField).first();
@@ -49,7 +84,14 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
});
it("should record user contact choice no", () => {
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
const wrapper = shallow(
<QueryCopilotFeedbackModal
explorer={new Explorer()}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
const contactAllowed = wrapper.find(ChoiceGroup);
contactAllowed.simulate("change", {}, { key: "no" });
@@ -60,7 +102,14 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
});
it("should record user contact choice yes", () => {
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
const wrapper = shallow(
<QueryCopilotFeedbackModal
explorer={new Explorer()}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
const contactAllowed = wrapper.find(ChoiceGroup);
contactAllowed.simulate("change", {}, { key: "yes" });
@@ -71,7 +120,14 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
});
it("should not render dont show again button", () => {
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
const wrapper = shallow(
<QueryCopilotFeedbackModal
explorer={new Explorer()}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
const dontShowAgain = wrapper.find(Checkbox);
@@ -80,8 +136,19 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
});
it("should render dont show again button and check it ", () => {
useQueryCopilot.getState().openFeedbackModal("test query", true, "test prompt");
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
mockUseCopilotStore.mockReturnValue({
...mockReturnValue,
showFeedbackModal: true,
likeQuery: true,
});
const wrapper = shallow(
<QueryCopilotFeedbackModal
explorer={new Explorer()}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
const dontShowAgain = wrapper.find(Checkbox);
dontShowAgain.simulate("change", {}, true);
@@ -92,7 +159,14 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
});
it("should cancel submission", () => {
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
const wrapper = shallow(
<QueryCopilotFeedbackModal
explorer={new Explorer()}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
const cancelButton = wrapper.find(DefaultButton);
cancelButton.simulate("click");
@@ -104,7 +178,14 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
it("should not submit submission if required description field is null", () => {
const explorer = new Explorer();
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={explorer} />);
const wrapper = shallow(
<QueryCopilotFeedbackModal
explorer={explorer}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
const submitButton = wrapper.find(PrimaryButton);
submitButton.simulate("click");
@@ -114,9 +195,15 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
});
it("should submit submission", () => {
useQueryCopilot.getState().openFeedbackModal("test query", false, "test prompt");
const explorer = new Explorer();
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={explorer} />);
const wrapper = shallow(
<QueryCopilotFeedbackModal
explorer={explorer}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
const submitButton = wrapper.find("form");
submitButton.simulate("submit");
@@ -124,6 +211,9 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
expect(SubmitFeedback).toHaveBeenCalledTimes(1);
expect(SubmitFeedback).toHaveBeenCalledWith({
containerId: "CopilotUserContainer",
databaseId: "CopilotUserDb",
mode: "User",
params: {
likeQuery: false,
generatedQuery: "test query",

View File

@@ -11,12 +11,22 @@ import {
TextField,
} from "@fluentui/react";
import Explorer from "Explorer/Explorer";
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
import { getUserEmail } from "../../../Utils/UserUtils";
export const QueryCopilotFeedbackModal = ({ explorer }: { explorer: Explorer }): JSX.Element => {
export const QueryCopilotFeedbackModal = ({
explorer,
databaseId,
containerId,
mode,
}: {
explorer: Explorer;
databaseId: string;
containerId: string;
mode: string;
}): JSX.Element => {
const {
generatedQuery,
userPrompt,
@@ -24,7 +34,7 @@ export const QueryCopilotFeedbackModal = ({ explorer }: { explorer: Explorer }):
showFeedbackModal,
closeFeedbackModal,
setHideFeedbackModalForLikedQueries,
} = useQueryCopilot();
} = useCopilotStore();
const [isContactAllowed, setIsContactAllowed] = React.useState<boolean>(false);
const [description, setDescription] = React.useState<string>("");
const [doNotShowAgainChecked, setDoNotShowAgainChecked] = React.useState<boolean>(false);
@@ -35,7 +45,10 @@ export const QueryCopilotFeedbackModal = ({ explorer }: { explorer: Explorer }):
setHideFeedbackModalForLikedQueries(doNotShowAgainChecked);
SubmitFeedback({
params: { generatedQuery, likeQuery, description, userPrompt, contact },
explorer: explorer,
explorer,
databaseId,
containerId,
mode: mode,
});
};

View File

@@ -1,7 +1,6 @@
import { IconButton, Image, Link, Modal, PrimaryButton, Stack, StackItem, Text } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
import React from "react";
import Database from "../../../../images/CopilotDatabase.svg";
import Flash from "../../../../images/CopilotFlash.svg";
import Thumb from "../../../../images/CopilotThumb.svg";
import CoplilotWelcomeIllustration from "../../../../images/CopliotWelcomeIllustration.svg";
@@ -23,8 +22,10 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
onDismiss={hideModal}
isBlocking={false}
styles={{
scrollableContent: {
minHeight: 680,
main: {
maxHeight: 530,
borderRadius: 10,
overflow: "hidden",
},
}}
>
@@ -52,7 +53,7 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
</Stack>
<Stack horizontalAlign="center">
<Stack.Item align="center" style={{ textAlign: "center" }}>
<Text className="title bold">Welcome to Copilot in Azure Cosmos DB (Private Preview)</Text>
<Text className="title bold">Welcome to Copilot in Azure Cosmos DB</Text>
</Stack.Item>
<Stack.Item align="center" className="text">
<Stack horizontal>
@@ -69,7 +70,7 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
<Text>
Ask Copilot to generate a query by describing the query in your words.
<br />
<Link target="_blank" href="https://aka.ms/cdb-copilot-learn-more">
<Link target="_blank" href="https://aka.ms/CopilotInAzureCDBDocs">
Learn more
</Link>
</Text>
@@ -87,31 +88,11 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
</StackItem>
</Stack>
<Text>
AI-generated content can have mistakes. Make sure its accurate and appropriate before using it.
AI-generated content can have mistakes. Make sure it is accurate and appropriate before executing the
query.
<br />
<Link target="_blank" href="https://aka.ms/cdb-copilot-preview-terms">
Read preview terms
</Link>
</Text>
</Stack.Item>
<Stack.Item align="center" className="text">
<Stack horizontal>
<StackItem align="start" className="imageTextPadding">
<Image src={Database} />
</StackItem>
<StackItem align="start">
<Text className="bold">
Query Copilot works on a sample database.
<br />
</Text>
</StackItem>
</Stack>
<Text>
While in Private Preview, Query Copilot is setup to work on sample database we have configured for you
at no cost.
<br />
<Link target="_blank" href="https://aka.ms/cdb-copilot-learn-more">
Learn more
Read our preview terms here
</Link>
</Text>
</Stack.Item>

View File

@@ -291,21 +291,6 @@ exports[`Query Copilot Feedback Modal snapshot test should cancel submission 1`]
for more information.
</Text>
<StyledCheckboxBase
checked={false}
label="Don't show me this next time"
onChange={[Function]}
styles={
Object {
"label": Object {
"paddingLeft": 0,
},
"root": Object {
"marginBottom": 14,
},
}
}
/>
<Stack
horizontal={true}
horizontalAlign="end"

View File

@@ -8,8 +8,10 @@ exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is
onDismiss={[Function]}
styles={
Object {
"scrollableContent": Object {
"minHeight": 680,
"main": Object {
"borderRadius": 10,
"maxHeight": 530,
"overflow": "hidden",
},
}
}
@@ -76,7 +78,7 @@ exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is
<Text
className="title bold"
>
Welcome to Copilot in Azure Cosmos DB (Private Preview)
Welcome to Copilot in Azure Cosmos DB
</Text>
</StackItem>
<StackItem
@@ -109,7 +111,7 @@ exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is
Ask Copilot to generate a query by describing the query in your words.
<br />
<StyledLinkBase
href="https://aka.ms/cdb-copilot-learn-more"
href="https://aka.ms/CopilotInAzureCDBDocs"
target="_blank"
>
Learn more
@@ -143,50 +145,13 @@ exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is
</StackItem>
</Stack>
<Text>
AI-generated content can have mistakes. Make sure its accurate and appropriate before using it.
AI-generated content can have mistakes. Make sure it is accurate and appropriate before executing the query.
<br />
<StyledLinkBase
href="https://aka.ms/cdb-copilot-preview-terms"
target="_blank"
>
Read preview terms
</StyledLinkBase>
</Text>
</StackItem>
<StackItem
align="center"
className="text"
>
<Stack
horizontal={true}
>
<StackItem
align="start"
className="imageTextPadding"
>
<Image
src={Object {}}
/>
</StackItem>
<StackItem
align="start"
>
<Text
className="bold"
>
Query Copilot works on a sample database.
<br />
</Text>
</StackItem>
</Stack>
<Text>
While in Private Preview, Query Copilot is setup to work on sample database we have configured for you at no cost.
<br />
<StyledLinkBase
href="https://aka.ms/cdb-copilot-learn-more"
target="_blank"
>
Learn more
Read our preview terms here
</StyledLinkBase>
</Text>
</StackItem>

View File

@@ -0,0 +1,135 @@
import { MinimalQueryIterator } from "Common/IteratorUtilities";
import { QueryResults } from "Contracts/ViewModels";
import { CopilotMessage } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { guid } from "Explorer/Tables/Utilities";
import { QueryCopilotState } from "hooks/useQueryCopilot";
import React, { createContext, useContext, useState } from "react";
import create from "zustand";
const context = createContext(null);
const useCopilotStore = (): Partial<QueryCopilotState> => useContext(context);
const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Element => {
const [useStore] = useState(() =>
create((set, get) => ({
generatedQuery: "",
likeQuery: false,
userPrompt: "",
showFeedbackModal: false,
hideFeedbackModalForLikedQueries: false,
correlationId: "",
query: "SELECT * FROM c",
selectedQuery: "",
isGeneratingQuery: false,
isGeneratingExplanation: false,
isExecuting: false,
dislikeQuery: undefined,
showCallout: false,
showSamplePrompts: false,
queryIterator: undefined,
queryResults: undefined,
errorMessage: "",
isSamplePromptsOpen: false,
showDeletePopup: false,
showFeedbackBar: false,
showCopyPopup: false,
showErrorMessageBar: false,
showInvalidQueryMessageBar: false,
generatedQueryComments: "",
wasCopilotUsed: false,
showWelcomeSidebar: true,
showCopilotSidebar: false,
chatMessages: [],
shouldIncludeInMessages: true,
showExplanationBubble: false,
isAllocatingContainer: false,
openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) =>
set({ generatedQuery, likeQuery, userPrompt, showFeedbackModal: true }),
closeFeedbackModal: () => set({ showFeedbackModal: false }),
setHideFeedbackModalForLikedQueries: (hideFeedbackModalForLikedQueries: boolean) =>
set({ hideFeedbackModalForLikedQueries }),
refreshCorrelationId: () => set({ correlationId: guid() }),
setUserPrompt: (userPrompt: string) => set({ userPrompt }),
setQuery: (query: string) => set({ query }),
setGeneratedQuery: (generatedQuery: string) => set({ generatedQuery }),
setSelectedQuery: (selectedQuery: string) => set({ selectedQuery }),
setIsGeneratingQuery: (isGeneratingQuery: boolean) => set({ isGeneratingQuery }),
setIsGeneratingExplanation: (isGeneratingExplanation: boolean) => set({ isGeneratingExplanation }),
setIsExecuting: (isExecuting: boolean) => set({ isExecuting }),
setLikeQuery: (likeQuery: boolean) => set({ likeQuery }),
setDislikeQuery: (dislikeQuery: boolean | undefined) => set({ dislikeQuery }),
setShowCallout: (showCallout: boolean) => set({ showCallout }),
setShowSamplePrompts: (showSamplePrompts: boolean) => set({ showSamplePrompts }),
setQueryIterator: (queryIterator: MinimalQueryIterator | undefined) => set({ queryIterator }),
setQueryResults: (queryResults: QueryResults | undefined) => set({ queryResults }),
setErrorMessage: (errorMessage: string) => set({ errorMessage }),
setIsSamplePromptsOpen: (isSamplePromptsOpen: boolean) => set({ isSamplePromptsOpen }),
setShowDeletePopup: (showDeletePopup: boolean) => set({ showDeletePopup }),
setShowFeedbackBar: (showFeedbackBar: boolean) => set({ showFeedbackBar }),
setshowCopyPopup: (showCopyPopup: boolean) => set({ showCopyPopup }),
setShowErrorMessageBar: (showErrorMessageBar: boolean) => set({ showErrorMessageBar }),
setShowInvalidQueryMessageBar: (showInvalidQueryMessageBar: boolean) => set({ showInvalidQueryMessageBar }),
setGeneratedQueryComments: (generatedQueryComments: string) => set({ generatedQueryComments }),
setWasCopilotUsed: (wasCopilotUsed: boolean) => set({ wasCopilotUsed }),
setShowWelcomeSidebar: (showWelcomeSidebar: boolean) => set({ showWelcomeSidebar }),
setShowCopilotSidebar: (showCopilotSidebar: boolean) => set({ showCopilotSidebar }),
setChatMessages: (chatMessages: CopilotMessage[]) => set({ chatMessages }),
setShouldIncludeInMessages: (shouldIncludeInMessages: boolean) => set({ shouldIncludeInMessages }),
setShowExplanationBubble: (showExplanationBubble: boolean) => set({ showExplanationBubble }),
getState: () => {
return get();
},
resetQueryCopilotStates: () => {
set((state) => ({
...state,
generatedQuery: "",
likeQuery: false,
userPrompt: "",
showFeedbackModal: false,
hideFeedbackModalForLikedQueries: false,
correlationId: "",
query: "SELECT * FROM c",
selectedQuery: "",
isGeneratingQuery: false,
isGeneratingExplanation: false,
isExecuting: false,
dislikeQuery: undefined,
showCallout: false,
showSamplePrompts: false,
queryIterator: undefined,
queryResults: undefined,
errorMessage: "",
isSamplePromptsOpen: false,
showDeletePopup: false,
showFeedbackBar: false,
showCopyPopup: false,
showErrorMessageBar: false,
showInvalidQueryMessageBar: false,
generatedQueryComments: "",
wasCopilotUsed: false,
showCopilotSidebar: false,
chatMessages: [],
shouldIncludeInMessages: true,
showExplanationBubble: false,
notebookServerInfo: {
notebookServerEndpoint: undefined,
authToken: undefined,
forwardingId: undefined,
},
containerStatus: {
status: undefined,
durationLeftInMinutes: undefined,
phoenixServerInfo: undefined,
},
isAllocatingContainer: false,
}));
},
})),
);
return <context.Provider value={useStore()}>{children}</context.Provider>;
};
export { CopilotProvider, useCopilotStore };

View File

@@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-console */
import {
Callout,
CommandBarButton,
@@ -17,20 +19,20 @@ import {
TextField,
} from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
import {
ContainerStatusType,
PoolIdType,
QueryCopilotSampleContainerSchema,
ShortenedQueryCopilotSampleContainerSchema,
} from "Common/Constants";
import { handleError } from "Common/ErrorHandlingUtils";
import { createUri } from "Common/UrlUtility";
import { WelcomeModal } from "Explorer/QueryCopilot/Modal/WelcomeModal";
import { CopyPopup } from "Explorer/QueryCopilot/Popup/CopyPopup";
import { DeletePopup } from "Explorer/QueryCopilot/Popup/DeletePopup";
import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import {
SuggestedPrompt,
getSampleDatabaseSuggestedPrompts,
getSuggestedPrompts,
} from "Explorer/QueryCopilot/QueryCopilotUtilities";
import { SubmitFeedback, allocatePhoenixContainer } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { GenerateSQLQueryResponse, QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { SamplePrompts, SamplePromptsProps } from "Explorer/QueryCopilot/Shared/SamplePrompts/SamplePrompts";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { userContext } from "UserContext";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React, { useRef, useState } from "react";
@@ -38,17 +40,17 @@ import HintIcon from "../../../images/Hint.svg";
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
import RecentIcon from "../../../images/Recent.svg";
import errorIcon from "../../../images/close-black.svg";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { useTabs } from "../../hooks/useTabs";
import { useCopilotStore } from "../QueryCopilot/QueryCopilotContext";
import { useSelectedNode } from "../useSelectedNode";
type QueryCopilotPromptProps = QueryCopilotProps & {
databaseId: string;
containerId: string;
toggleCopilot: (toggle: boolean) => void;
};
interface SuggestedPrompt {
id: number;
text: string;
}
const promptStyles: IButtonStyles = {
root: { border: 0, selectors: { ":hover": { outline: "1px dashed #605e5c" } } },
label: { fontWeight: 400, textAlign: "left", paddingLeft: 8 },
@@ -57,10 +59,13 @@ const promptStyles: IButtonStyles = {
export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
explorer,
toggleCopilot,
databaseId,
containerId,
}: QueryCopilotPromptProps): JSX.Element => {
const [copilotTeachingBubbleVisible, { toggle: toggleCopilotTeachingBubbleVisible }] = useBoolean(false);
const inputEdited = useRef(false);
const {
openFeedbackModal,
hideFeedbackModalForLikedQueries,
userPrompt,
setUserPrompt,
@@ -93,7 +98,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
setGeneratedQueryComments,
setQueryResults,
setErrorMessage,
} = useQueryCopilot();
} = useCopilotStore();
const sampleProps: SamplePromptsProps = {
isSamplePromptsOpen: isSamplePromptsOpen,
@@ -118,14 +123,13 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
}, 6000);
};
const isSampleCopilotActive = useSelectedNode.getState().isQueryCopilotCollectionSelected();
const cachedHistoriesString = localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotHistories`);
const cachedHistories = cachedHistoriesString?.split("|");
const [histories, setHistories] = useState<string[]>(cachedHistories || []);
const suggestedPrompts: SuggestedPrompt[] = [
{ id: 1, text: 'Show all products that have the word "ultra" in the name or description' },
{ id: 2, text: "What are all of the possible categories for the products, and their counts?" },
{ id: 3, text: 'Show me all products that have been reviewed by someone with a username that contains "bob"' },
];
const suggestedPrompts: SuggestedPrompt[] = isSampleCopilotActive
? getSampleDatabaseSuggestedPrompts()
: getSuggestedPrompts();
const [filteredHistories, setFilteredHistories] = useState<string[]>(histories);
const [filteredSuggestedPrompts, setFilteredSuggestedPrompts] = useState<SuggestedPrompt[]>(suggestedPrompts);
@@ -176,16 +180,11 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
setShowDeletePopup(false);
useTabs.getState().setIsTabExecuting(true);
useTabs.getState().setIsQueryErrorThrown(false);
if (
useQueryCopilot.getState().containerStatus.status !== ContainerStatusType.Active &&
!userContext.features.disableCopilotPhoenixGateaway
) {
await explorer.allocateContainer(PoolIdType.QueryCopilot);
}
const mode: string = isSampleCopilotActive ? "Sample" : "User";
await allocatePhoenixContainer({ explorer, databaseId, containerId, mode });
const payload = {
containerSchema: userContext.features.enableCopilotFullSchema
? QueryCopilotSampleContainerSchema
: ShortenedQueryCopilotSampleContainerSchema,
userPrompt: userPrompt,
};
useQueryCopilot.getState().refreshCorrelationId();
@@ -198,6 +197,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
headers: {
"content-type": "application/json",
"x-ms-correlationid": useQueryCopilot.getState().correlationId,
Authorization: `token ${useQueryCopilot.getState().notebookServerInfo.authToken}`,
},
body: JSON.stringify(payload),
});
@@ -215,13 +215,30 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
setGeneratedQueryComments(generateSQLQueryResponse.explanation);
setShowFeedbackBar(true);
resetQueryResults();
TelemetryProcessor.traceSuccess(Action.QueryGenerationFromCopilotPrompt, {
databaseName: databaseId,
collectionId: containerId,
copilotLatency:
Date.parse(generateSQLQueryResponse?.generateEnd) - Date.parse(generateSQLQueryResponse?.generateStart),
responseCode: response.status,
});
} else {
setShowInvalidQueryMessageBar(true);
TelemetryProcessor.traceFailure(Action.QueryGenerationFromCopilotPrompt, {
databaseName: databaseId,
collectionId: containerId,
responseCode: response.status,
});
}
} else {
handleError(JSON.stringify(generateSQLQueryResponse), "copilotInternalServerError");
useTabs.getState().setIsQueryErrorThrown(true);
setShowErrorMessageBar(true);
TelemetryProcessor.traceFailure(Action.QueryGenerationFromCopilotPrompt, {
databaseName: databaseId,
collectionId: containerId,
responseCode: response.status,
});
}
} catch (error) {
handleError(error, "executeNaturalLanguageQuery");
@@ -310,6 +327,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
styles={{ root: { width: "95%" }, fieldGroup: { borderRadius: 6 } }}
disabled={isGeneratingQuery}
autoComplete="off"
placeholder="Ask a question in natural language and well generate the query for you."
/>
{copilotTeachingBubbleVisible && (
<TeachingBubble
@@ -489,7 +507,10 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
description: "",
userPrompt: userPrompt,
},
explorer: explorer,
explorer,
databaseId,
containerId,
mode: isSampleCopilotActive ? "Sample" : "User",
});
}}
directionalHint={DirectionalHint.topCenter}
@@ -499,7 +520,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
<Link
onClick={() => {
setShowCallout(false);
useQueryCopilot.getState().openFeedbackModal(generatedQuery, true, userPrompt);
openFeedbackModal(generatedQuery, true, userPrompt);
}}
>
more feedback?
@@ -524,7 +545,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }}
onClick={() => {
if (!dislikeQuery) {
useQueryCopilot.getState().openFeedbackModal(generatedQuery, false, userPrompt);
openFeedbackModal(generatedQuery, false, userPrompt);
setLikeQuery(false);
}
setDislikeQuery(!dislikeQuery);

View File

@@ -1,5 +1,6 @@
/* eslint-disable no-console */
import { Stack } from "@fluentui/react";
import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
@@ -11,6 +12,7 @@ import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotRe
import { userContext } from "UserContext";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { useSidePanel } from "hooks/useSidePanel";
import { ReactTabKind, TabsState, useTabs } from "hooks/useTabs";
import React, { useState } from "react";
import SplitterLayout from "react-splitter-layout";
import QueryCommandIcon from "../../../images/CopilotCommand.svg";
@@ -28,13 +30,14 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
? StringUtility.toBoolean(cachedCopilotToggleStatus)
: true;
const [copilotActive, setCopilotActive] = useState<boolean>(copilotInitialActive);
const [tabActive, setTabActive] = useState<boolean>(true);
const getCommandbarButtons = (): CommandButtonComponentProps[] => {
const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query";
const executeQueryBtn = {
iconSrc: ExecuteQueryIcon,
iconAlt: executeQueryBtnLabel,
onCommandClick: () => OnExecuteQueryClick(),
onCommandClick: () => OnExecuteQueryClick(useQueryCopilot),
commandButtonLabel: executeQueryBtnLabel,
ariaLabel: executeQueryBtnLabel,
hasPopup: false,
@@ -73,10 +76,13 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
React.useEffect(() => {
return () => {
const commandbarButtons = getCommandbarButtons();
commandbarButtons.pop();
commandbarButtons.map((props: CommandButtonComponentProps) => (props.disabled = true));
useCommandBar.getState().setContextButtons(commandbarButtons);
useTabs.subscribe((state: TabsState) => {
if (state.activeReactTab === ReactTabKind.QueryCopilot) {
setTabActive(true);
} else {
setTabActive(false);
}
});
};
}, []);
@@ -88,8 +94,13 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
return (
<Stack className="tab-pane" style={{ width: "100%" }}>
<div style={isGeneratingQuery ? { height: "100%" } : { overflowY: "auto", height: "100%" }}>
{copilotActive && (
<QueryCopilotPromptbar explorer={explorer} toggleCopilot={toggleCopilot}></QueryCopilotPromptbar>
{tabActive && copilotActive && (
<QueryCopilotPromptbar
explorer={explorer}
toggleCopilot={toggleCopilot}
databaseId={QueryCopilotSampleDatabaseId}
containerId={QueryCopilotSampleContainerId}
></QueryCopilotPromptbar>
)}
<Stack className="tabPaneContentContainer">
<SplitterLayout percentage={true} vertical={true} primaryIndex={0} primaryMinSize={30} secondaryMinSize={70}>

View File

@@ -7,6 +7,11 @@ import { getCommonQueryOptions } from "Common/dataAccess/queryDocuments";
import DocumentId from "Explorer/Tree/DocumentId";
import { logConsoleProgress } from "Utils/NotificationConsoleUtils";
export interface SuggestedPrompt {
id: number;
text: string;
}
export const querySampleDocuments = (query: string, options: FeedOptions): QueryIterator<ItemDefinition & Resource> => {
options = getCommonQueryOptions(options);
return sampleDataClient()
@@ -33,3 +38,19 @@ export const readSampleDocument = async (documentId: DocumentId): Promise<Item>
clearMessage();
}
};
export const getSampleDatabaseSuggestedPrompts = (): SuggestedPrompt[] => {
return [
{ id: 1, text: 'Show all products that have the word "ultra" in the name or description' },
{ id: 2, text: "What are all of the possible categories for the products, and their counts?" },
{ id: 3, text: 'Show me all products that have been reviewed by someone with a username that contains "bob"' },
];
};
export const getSuggestedPrompts = (): SuggestedPrompt[] => {
return [
{ id: 1, text: "Show the first 10 items" },
{ id: 2, text: 'Count all the items in my data as "numItems"' },
{ id: 3, text: "Find the oldest item added to my collection" },
];
};

View File

@@ -1,4 +1,3 @@
import { QueryCopilotSampleContainerSchema, ShortenedQueryCopilotSampleContainerSchema } from "Common/Constants";
import { handleError } from "Common/ErrorHandlingUtils";
import { createUri } from "Common/UrlUtility";
import Explorer from "Explorer/Explorer";
@@ -37,9 +36,6 @@ describe("Query Copilot Client", () => {
userPrompt: "UserPrompt",
description: "Description",
contact: "Contact",
containerSchema: userContext.features.enableCopilotFullSchema
? QueryCopilotSampleContainerSchema
: ShortenedQueryCopilotSampleContainerSchema,
};
const mockStore = useQueryCopilot.getState();
@@ -59,6 +55,9 @@ describe("Query Copilot Client", () => {
globalThis.fetch = mockFetch;
await SubmitFeedback({
databaseId: "test",
containerId: "test",
mode: "User",
params: {
likeQuery: true,
generatedQuery: "GeneratedQuery",
@@ -91,6 +90,9 @@ describe("Query Copilot Client", () => {
globalThis.fetch = mockFetch;
await SubmitFeedback({
databaseId: "test",
containerId: "test",
mode: "User",
params: {
likeQuery: false,
generatedQuery: "GeneratedQuery",
@@ -108,6 +110,7 @@ describe("Query Copilot Client", () => {
headers: {
"content-type": "application/json",
"x-ms-correlationid": "mocked-correlation-id",
Authorization: "token mocked-token",
},
}),
);
@@ -120,6 +123,9 @@ describe("Query Copilot Client", () => {
globalThis.fetch = jest.fn().mockRejectedValueOnce(new Error("Mock error"));
await SubmitFeedback({
databaseId: "test",
containerId: "test",
mode: "User",
params: {
likeQuery: true,
generatedQuery: "GeneratedQuery",

View File

@@ -1,28 +1,174 @@
import { FeedOptions } from "@azure/cosmos";
import {
Areas,
ConnectionStatusType,
ContainerStatusType,
HttpStatusCodes,
PoolIdType,
QueryCopilotSampleContainerId,
QueryCopilotSampleContainerSchema,
ShortenedQueryCopilotSampleContainerSchema,
} from "Common/Constants";
import { getErrorMessage, handleError } from "Common/ErrorHandlingUtils";
import { getErrorMessage, getErrorStack, handleError } from "Common/ErrorHandlingUtils";
import { shouldEnableCrossPartitionKey } from "Common/HeadersUtility";
import { MinimalQueryIterator } from "Common/IteratorUtilities";
import { createUri } from "Common/UrlUtility";
import { queryDocumentsPage } from "Common/dataAccess/queryDocumentsPage";
import { QueryResults } from "Contracts/ViewModels";
import { configContext } from "ConfigContext";
import {
ContainerConnectionInfo,
CopilotEnabledConfiguration,
FeatureRegistration,
IProvisionData,
} from "Contracts/DataModels";
import { AuthorizationTokenHeaderMetadata, QueryResults } from "Contracts/ViewModels";
import { useDialog } from "Explorer/Controls/Dialog";
import Explorer from "Explorer/Explorer";
import { querySampleDocuments } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import { FeedbackParams, GenerateSQLQueryResponse } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext";
import { getAuthorizationHeader } from "Utils/AuthorizationUtils";
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
import { useTabs } from "hooks/useTabs";
import * as StringUtility from "../../../Shared/StringUtility";
export const isCopilotFeatureRegistered = async (subscriptionId: string): Promise<boolean> => {
const api_version = "2021-07-01";
const url = `${configContext.ARM_ENDPOINT}/subscriptions/${subscriptionId}/providers/Microsoft.Features/featureProviders/Microsoft.DocumentDB/subscriptionFeatureRegistrations/CopilotInAzureCDB?api-version=${api_version}`;
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
const headers = { [authorizationHeader.header]: authorizationHeader.token };
let response;
try {
response = await window.fetch(url, {
headers,
});
} catch (error) {
return false;
}
if (!response?.ok) {
return false;
}
const featureRegistration = (await response?.json()) as FeatureRegistration;
return featureRegistration?.properties?.state === "Registered";
};
export const getCopilotEnabled = async (): Promise<boolean> => {
const url = `${configContext.BACKEND_ENDPOINT}/api/portalsettings/querycopilot`;
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
const headers = { [authorizationHeader.header]: authorizationHeader.token };
let response;
try {
response = await window.fetch(url, {
headers,
});
} catch (error) {
return false;
}
if (!response?.ok) {
throw new Error(await response?.text());
}
const copilotPortalConfiguration = (await response?.json()) as CopilotEnabledConfiguration;
return copilotPortalConfiguration?.isEnabled;
};
export const allocatePhoenixContainer = async ({
explorer,
databaseId,
containerId,
mode,
}: {
explorer: Explorer;
databaseId: string;
containerId: string;
mode: string;
}): Promise<void> => {
try {
if (
useQueryCopilot.getState().containerStatus.status !== ContainerStatusType.Active &&
!userContext.features.disableCopilotPhoenixGateaway
) {
await explorer.allocateContainer(PoolIdType.QueryCopilot, mode);
} else {
const currentAllocatedSchemaInfo = useQueryCopilot.getState().schemaAllocationInfo;
if (
currentAllocatedSchemaInfo.databaseId !== databaseId ||
currentAllocatedSchemaInfo.containerId !== containerId
) {
await resetPhoenixContainerSchema({ explorer, databaseId, containerId, mode });
}
}
useQueryCopilot.getState().setSchemaAllocationInfo({
databaseId,
containerId,
});
} catch (error) {
traceFailure(Action.PhoenixConnection, {
dataExplorerArea: Areas.Copilot,
status: error.status,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
});
useQueryCopilot.getState().resetContainerConnection();
if (error?.status === HttpStatusCodes.Forbidden && error.message) {
useDialog.getState().showOkModalDialog("Connection Failed", `${error.message}`);
} else {
useDialog
.getState()
.showOkModalDialog(
"Connection Failed",
"We are unable to connect to the temporary workspace. Please try again in a few minutes. If the error persists, file a support ticket.",
);
}
} finally {
useTabs.getState().setIsTabExecuting(false);
}
};
export const resetPhoenixContainerSchema = async ({
explorer,
databaseId,
containerId,
mode,
}: {
explorer: Explorer;
databaseId: string;
containerId: string;
mode: string;
}): Promise<void> => {
try {
const provisionData: IProvisionData = {
poolId: PoolIdType.QueryCopilot,
databaseId: databaseId,
containerId: containerId,
mode: mode,
};
const connectionInfo = await explorer.phoenixClient.allocateContainer(provisionData);
const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.Connecting,
};
await explorer.setNotebookInfo(false, connectionInfo, connectionStatus);
} catch (error) {
traceFailure(Action.PhoenixConnection, {
dataExplorerArea: Areas.Copilot,
status: error.status,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
});
throw error;
}
};
export const SendQueryRequest = async ({
userPrompt,
explorer,
@@ -106,16 +252,19 @@ export const SendQueryRequest = async ({
export const SubmitFeedback = async ({
params,
explorer,
databaseId,
containerId,
mode,
}: {
params: FeedbackParams;
explorer: Explorer;
databaseId: string;
containerId: string;
mode: string;
}): Promise<void> => {
try {
const { likeQuery, generatedQuery, userPrompt, description, contact } = params;
const payload = {
containerSchema: userContext.features.enableCopilotFullSchema
? QueryCopilotSampleContainerSchema
: ShortenedQueryCopilotSampleContainerSchema,
like: likeQuery ? "like" : "dislike",
generatedSql: generatedQuery,
userPrompt,
@@ -126,7 +275,7 @@ export const SubmitFeedback = async ({
useQueryCopilot.getState().containerStatus.status !== ContainerStatusType.Active &&
!userContext.features.disableCopilotPhoenixGateaway
) {
await explorer.allocateContainer(PoolIdType.QueryCopilot);
await allocatePhoenixContainer({ explorer, databaseId, containerId, mode });
}
const serverInfo = useQueryCopilot.getState().notebookServerInfo;
const feedbackUri = userContext.features.disableCopilotPhoenixGateaway
@@ -137,6 +286,7 @@ export const SubmitFeedback = async ({
headers: {
"content-type": "application/json",
"x-ms-correlationid": useQueryCopilot.getState().correlationId,
Authorization: `token ${useQueryCopilot.getState().notebookServerInfo.authToken}`,
},
body: JSON.stringify(payload),
});
@@ -145,7 +295,7 @@ export const SubmitFeedback = async ({
}
};
export const OnExecuteQueryClick = async (): Promise<void> => {
export const OnExecuteQueryClick = async (useQueryCopilot: Partial<QueryCopilotState>): Promise<void> => {
traceStart(Action.ExecuteQueryGeneratedFromQueryCopilot, {
correlationId: useQueryCopilot.getState().correlationId,
userPrompt: useQueryCopilot.getState().userPrompt,
@@ -160,13 +310,14 @@ export const OnExecuteQueryClick = async (): Promise<void> => {
useQueryCopilot.getState().setQueryIterator(queryIterator);
setTimeout(async () => {
await QueryDocumentsPerPage(0, queryIterator);
await QueryDocumentsPerPage(0, queryIterator, useQueryCopilot);
}, 100);
};
export const QueryDocumentsPerPage = async (
firstItemIndex: number,
queryIterator: MinimalQueryIterator,
useQueryCopilot: Partial<QueryCopilotState>,
): Promise<void> => {
try {
useQueryCopilot.getState().setIsExecuting(true);

View File

@@ -32,3 +32,8 @@ export interface FeedbackParams {
export interface QueryCopilotProps {
explorer: Explorer;
}
export interface CopilotSchemaAllocationInfo {
databaseId: string;
containerId: string;
}

View File

@@ -12,7 +12,7 @@ export const QueryCopilotResults: React.FC = (): JSX.Element => {
queryResults={useQueryCopilot.getState().queryResults}
isExecuting={useQueryCopilot.getState().isExecuting}
executeQueryDocumentsPage={(firstItemIndex: number) =>
QueryDocumentsPerPage(firstItemIndex, useQueryCopilot.getState().queryIterator)
QueryDocumentsPerPage(firstItemIndex, useQueryCopilot.getState().queryIterator, useQueryCopilot)
}
/>
);

View File

@@ -18,6 +18,8 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
}
>
<QueryCopilotPromptbar
containerId="SampleContainer"
databaseId="CopilotSampleDb"
explorer={
Explorer {
"_isInitializingNotebooks": false,