mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-06-30 02:28:44 +01:00
Add the vector policy source embedding controls
This commit is contained in:
@@ -60,9 +60,6 @@ export const VectorEmbeddingSourceComponent: FunctionComponent<IVectorEmbeddingS
|
|||||||
discardChanges,
|
discardChanges,
|
||||||
onChange,
|
onChange,
|
||||||
}): JSX.Element => {
|
}): JSX.Element => {
|
||||||
// The integrated-embedding capability gate lives at the parent's call site
|
|
||||||
// (VectorEmbeddingPoliciesComponent). Don't add a gate here — gating before the
|
|
||||||
// hooks below would violate the Rules of Hooks.
|
|
||||||
const suffix = index + 1;
|
const suffix = index + 1;
|
||||||
|
|
||||||
const [sourcePathsRaw, setSourcePathsRaw] = useState<string>(initialEmbeddingSource?.sourcePaths?.join(", ") || "");
|
const [sourcePathsRaw, setSourcePathsRaw] = useState<string>(initialEmbeddingSource?.sourcePaths?.join(", ") || "");
|
||||||
@@ -90,10 +87,7 @@ export const VectorEmbeddingSourceComponent: FunctionComponent<IVectorEmbeddingS
|
|||||||
|
|
||||||
const isValid = !hasAnyValue || (!sourcePathsError && !deploymentNameError && !modelNameError && !endpointError);
|
const isValid = !hasAnyValue || (!sourcePathsError && !deploymentNameError && !modelNameError && !endpointError);
|
||||||
|
|
||||||
// Memoize the synthesized source so that equal field values yield the same object reference.
|
// Memoize to avoid infinite render loops from parent's reference-equality check.
|
||||||
// Without this, every render would mint a new object literal, defeating the parent's
|
|
||||||
// reference-equality guard in onEmbeddingSourceChange and producing an infinite render loop
|
|
||||||
// once all five fields become valid.
|
|
||||||
const source = React.useMemo<VectorEmbeddingSource | undefined>(() => {
|
const source = React.useMemo<VectorEmbeddingSource | undefined>(() => {
|
||||||
if (!hasAnyValue || !isValid) {
|
if (!hasAnyValue || !isValid) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -120,12 +114,6 @@ export const VectorEmbeddingSourceComponent: FunctionComponent<IVectorEmbeddingS
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
onChange(source, isValid);
|
onChange(source, isValid);
|
||||||
// `onChange` is intentionally omitted from the dependency array. The parent recreates
|
|
||||||
// its inline arrow on every render, and including it here would re-fire this effect
|
|
||||||
// on every parent render — feeding back into the parent's setState and producing an
|
|
||||||
// infinite loop. The effect's behavior depends only on `source` / `isValid`, which are
|
|
||||||
// memoized from local state, so capturing a stale `onChange` reference is safe: the
|
|
||||||
// underlying state mutation (setVectorEmbeddingPolicyData) is identity-stable.
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [source, isValid]);
|
}, [source, isValid]);
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ export const getQuantizerTypeOptions = (): IDropdownOption[] => [
|
|||||||
export const supportsQuantization = (indexType: VectorIndex["type"] | "none" | undefined): boolean =>
|
export const supportsQuantization = (indexType: VectorIndex["type"] | "none" | undefined): boolean =>
|
||||||
indexType === "quantizedFlat" || indexType === "diskANN";
|
indexType === "quantizedFlat" || indexType === "diskANN";
|
||||||
|
|
||||||
// Parses a comma-separated path list, trims whitespace, drops blanks, and
|
|
||||||
// prefixes a leading "/" on each entry if it isn't there already.
|
|
||||||
export const parseSourcePaths = (raw: string): string[] => {
|
export const parseSourcePaths = (raw: string): string[] => {
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -35,10 +35,8 @@ export const isFullTextSearchPreviewFeaturesEnabled = (targetAccountOverride?: A
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// `enableEmbeddingGenerator` is a top-level boolean on the database account
|
// Gates the `embeddingSource` block inside a Container Vector Policy.
|
||||||
// (e.g. properties.enableEmbeddingGenerator), not an entry in
|
// Uses the top-level `enableEmbeddingGenerator` boolean (not capabilities[]).
|
||||||
// properties.capabilities[]. It's the Foundry integrated embedding generator
|
|
||||||
// flag that gates the `embeddingSource` block inside a Container Vector Policy.
|
|
||||||
export const isIntegratedEmbeddingEnabled = (targetAccountOverride?: AccountOverride): boolean => {
|
export const isIntegratedEmbeddingEnabled = (targetAccountOverride?: AccountOverride): boolean => {
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const enableEmbeddingGenerator =
|
const enableEmbeddingGenerator =
|
||||||
|
|||||||
@@ -1413,25 +1413,15 @@ export interface VectorEmbedding {
|
|||||||
/* The number of dimensions in the vector. */
|
/* The number of dimensions in the vector. */
|
||||||
dimensions: number;
|
dimensions: number;
|
||||||
|
|
||||||
/* Optional configuration for Cosmos DB integrated embeddings (server-side
|
/* Added manually pending upstream Swagger update; `npm run generateARMClients` will overwrite. */
|
||||||
embedding generation via Microsoft Foundry). When set, Cosmos DB
|
|
||||||
auto-populates the vector field from the listed source paths.
|
|
||||||
TODO: This field was added manually pending an upstream Swagger update;
|
|
||||||
re-applying `npm run generateARMClients` will overwrite it. */
|
|
||||||
embeddingSource?: VectorEmbeddingSource;
|
embeddingSource?: VectorEmbeddingSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Configuration for Cosmos DB integrated embeddings. */
|
|
||||||
export interface VectorEmbeddingSource {
|
export interface VectorEmbeddingSource {
|
||||||
/* Source field paths used as input for embedding generation. */
|
|
||||||
sourcePaths: string[];
|
sourcePaths: string[];
|
||||||
/* The Foundry deployment name. */
|
|
||||||
deploymentName: string;
|
deploymentName: string;
|
||||||
/* The Foundry model name. */
|
|
||||||
modelName: string;
|
modelName: string;
|
||||||
/* The Foundry endpoint URL. */
|
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
/* Authentication type used by Cosmos DB to call Foundry. */
|
|
||||||
authType: "Entra";
|
authType: "Entra";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,21 +57,17 @@ test("SQL container with vector embedding policy and embedding source", async ({
|
|||||||
|
|
||||||
const explorer = await DataExplorer.open(page, TestAccount.SQL);
|
const explorer = await DataExplorer.open(page, TestAccount.SQL);
|
||||||
|
|
||||||
// Open the New Container panel and check if the embedding source capability is available
|
|
||||||
// before proceeding. We must skip before whilePanelOpen to avoid a timeout on panel close.
|
|
||||||
const newContainerButton = await explorer.globalCommandButton("New Container");
|
const newContainerButton = await explorer.globalCommandButton("New Container");
|
||||||
await newContainerButton.click();
|
await newContainerButton.click();
|
||||||
|
|
||||||
const panel = explorer.panel("New Container");
|
const panel = explorer.panel("New Container");
|
||||||
await panel.waitFor();
|
await panel.waitFor();
|
||||||
|
|
||||||
// Expand vector policy section and add a vector embedding to check for the embedding source accordion
|
|
||||||
await panel.getByTestId("ContainerVectorPolicy/Section").click();
|
await panel.getByTestId("ContainerVectorPolicy/Section").click();
|
||||||
await panel.getByTestId("VectorEmbedding/AddButton").click();
|
await panel.getByTestId("VectorEmbedding/AddButton").click();
|
||||||
|
|
||||||
const embeddingSourceSection = panel.getByTestId("VectorEmbeddingSource/Section/1");
|
const embeddingSourceSection = panel.getByTestId("VectorEmbeddingSource/Section/1");
|
||||||
if ((await embeddingSourceSection.count()) === 0) {
|
if ((await embeddingSourceSection.count()) === 0) {
|
||||||
// Close the panel before skipping
|
|
||||||
await panel.getByRole("button", { name: "Close New Container" }).click();
|
await panel.getByRole("button", { name: "Close New Container" }).click();
|
||||||
await panel.waitFor({ state: "detached" });
|
await panel.waitFor({ state: "detached" });
|
||||||
test.skip(true, "Test account does not have the integrated embedding capability.");
|
test.skip(true, "Test account does not have the integrated embedding capability.");
|
||||||
@@ -85,7 +81,7 @@ test("SQL container with vector embedding policy and embedding source", async ({
|
|||||||
await panel.getByTestId("VectorEmbedding/Path/1").fill("/embedding");
|
await panel.getByTestId("VectorEmbedding/Path/1").fill("/embedding");
|
||||||
await panel.getByTestId("VectorEmbedding/Dimensions/1").fill("1536");
|
await panel.getByTestId("VectorEmbedding/Dimensions/1").fill("1536");
|
||||||
|
|
||||||
// Expand embedding source section and fill fields
|
// Expand embedding source and fill fields
|
||||||
await embeddingSourceSection.click();
|
await embeddingSourceSection.click();
|
||||||
|
|
||||||
await panel.getByTestId("VectorEmbeddingSource/SourcePaths/1").fill("/description");
|
await panel.getByTestId("VectorEmbeddingSource/SourcePaths/1").fill("/description");
|
||||||
@@ -102,7 +98,6 @@ test("SQL container with vector embedding policy and embedding source", async ({
|
|||||||
const databaseNode = await explorer.waitForNode(databaseId);
|
const databaseNode = await explorer.waitForNode(databaseId);
|
||||||
const containerNode = await explorer.waitForContainerNode(databaseId, containerId);
|
const containerNode = await explorer.waitForContainerNode(databaseId, containerId);
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
await containerNode.openContextMenu();
|
await containerNode.openContextMenu();
|
||||||
await containerNode.contextMenuItem("Delete Container").click();
|
await containerNode.contextMenuItem("Delete Container").click();
|
||||||
await explorer.whilePanelOpen(
|
await explorer.whilePanelOpen(
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ test.describe("Vector Policy under Scale & Settings", () => {
|
|||||||
await explorer.frame.getByTestId("VectorEmbedding/Path/1").fill("/embedding");
|
await explorer.frame.getByTestId("VectorEmbedding/Path/1").fill("/embedding");
|
||||||
await explorer.frame.getByTestId("VectorEmbedding/Dimensions/1").fill("1536");
|
await explorer.frame.getByTestId("VectorEmbedding/Dimensions/1").fill("1536");
|
||||||
|
|
||||||
// Expand embedding source section and fill fields
|
// Expand embedding source and fill fields
|
||||||
await embeddingSourceSection.click();
|
await embeddingSourceSection.click();
|
||||||
|
|
||||||
await explorer.frame.getByTestId("VectorEmbeddingSource/SourcePaths/1").fill("/description");
|
await explorer.frame.getByTestId("VectorEmbeddingSource/SourcePaths/1").fill("/description");
|
||||||
@@ -213,7 +213,6 @@ test.describe("Vector Policy under Scale & Settings", () => {
|
|||||||
.getByTestId("VectorEmbeddingSource/Endpoint/1")
|
.getByTestId("VectorEmbeddingSource/Endpoint/1")
|
||||||
.fill("https://e2e-embedding.cognitiveservices.azure.com/");
|
.fill("https://e2e-embedding.cognitiveservices.azure.com/");
|
||||||
|
|
||||||
// Save changes
|
|
||||||
const saveButton = explorer.commandBarButton(CommandBarButton.Save);
|
const saveButton = explorer.commandBarButton(CommandBarButton.Save);
|
||||||
await expect(saveButton).toBeEnabled();
|
await expect(saveButton).toBeEnabled();
|
||||||
await saveButton.click();
|
await saveButton.click();
|
||||||
@@ -224,7 +223,6 @@ test.describe("Vector Policy under Scale & Settings", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("Existing embedding source fields are disabled after save", async () => {
|
test("Existing embedding source fields are disabled after save", async () => {
|
||||||
// Ensure a policy with embedding source exists (from previous test or create one)
|
|
||||||
const sourcePathsInput = explorer.frame.getByTestId("VectorEmbeddingSource/SourcePaths/1");
|
const sourcePathsInput = explorer.frame.getByTestId("VectorEmbeddingSource/SourcePaths/1");
|
||||||
if ((await sourcePathsInput.count()) === 0) {
|
if ((await sourcePathsInput.count()) === 0) {
|
||||||
test.skip(true, "No embedding source present; previous test may have been skipped.");
|
test.skip(true, "No embedding source present; previous test may have been skipped.");
|
||||||
|
|||||||
Reference in New Issue
Block a user