mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-09-19 09:02:41 +01:00
Add embedding source validation rules
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -150,30 +150,54 @@ describe("VectorEmbeddingPoliciesComponent - embedding source", () => {
|
|||||||
});
|
});
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("At least one source path is required")).toBeInTheDocument();
|
expect(screen.getByText("At least one source path is required")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Model name is required")).toBeInTheDocument();
|
expect(screen.getByText("Embedding model name is required")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Endpoint is required")).toBeInTheDocument();
|
expect(screen.getByText("Microsoft Foundry Endpoint is required")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
const last = onChange.mock.calls[onChange.mock.calls.length - 1];
|
const last = onChange.mock.calls[onChange.mock.calls.length - 1];
|
||||||
expect(last[2]).toBe(false);
|
expect(last[2]).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("invalid endpoint shows the https:// error", async () => {
|
test("source paths must start with slash and differ from vector path", async () => {
|
||||||
|
expandSection();
|
||||||
|
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-sourcePaths-1"), {
|
||||||
|
target: { value: "description" },
|
||||||
|
});
|
||||||
|
await waitFor(() => expect(screen.getByText("Source paths must start with /")).toBeInTheDocument());
|
||||||
|
|
||||||
|
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-sourcePaths-1"), {
|
||||||
|
target: { value: "/vector2" },
|
||||||
|
});
|
||||||
|
await waitFor(() => expect(screen.getByText("Source path must be different from vector path")).toBeInTheDocument());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("invalid endpoint shows the Azure OpenAI or Foundry URL error", async () => {
|
||||||
expandSection();
|
expandSection();
|
||||||
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-endpoint-1"), {
|
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-endpoint-1"), {
|
||||||
target: { value: "not-a-url" },
|
target: { value: "not-a-url" },
|
||||||
});
|
});
|
||||||
await waitFor(() => expect(screen.getByText("Endpoint must be a valid https:// URL")).toBeInTheDocument());
|
await waitFor(() =>
|
||||||
|
expect(screen.getByText("Endpoint must be a valid Azure OpenAI or Foundry https:// URL")).toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
|
||||||
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-endpoint-1"), {
|
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-endpoint-1"), {
|
||||||
target: { value: "http://insecure.example.com" },
|
target: { value: "http://insecure.example.com" },
|
||||||
});
|
});
|
||||||
await waitFor(() => expect(screen.getByText("Endpoint must be a valid https:// URL")).toBeInTheDocument());
|
await waitFor(() =>
|
||||||
|
expect(screen.getByText("Endpoint must be a valid Azure OpenAI or Foundry https:// URL")).toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-endpoint-1"), {
|
||||||
|
target: { value: "https://example.com" },
|
||||||
|
});
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByText("Endpoint must be a valid Azure OpenAI or Foundry https:// URL")).toBeInTheDocument(),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("valid input propagates an embeddingSource with parsed sourcePaths", async () => {
|
test("valid input propagates an embeddingSource with parsed sourcePaths", async () => {
|
||||||
expandSection();
|
expandSection();
|
||||||
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-sourcePaths-1"), {
|
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-sourcePaths-1"), {
|
||||||
target: { value: "/description, title" },
|
target: { value: "/description, /title" },
|
||||||
});
|
});
|
||||||
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-deploymentName-1"), {
|
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-deploymentName-1"), {
|
||||||
target: { value: "my-deployment" },
|
target: { value: "my-deployment" },
|
||||||
@@ -213,7 +237,7 @@ describe("VectorEmbeddingPoliciesComponent - embedding source", () => {
|
|||||||
fireEvent.change(sourcePaths, { target: { value: "/description" } });
|
fireEvent.change(sourcePaths, { target: { value: "/description" } });
|
||||||
fireEvent.change(deploymentName, { target: { value: "d" } });
|
fireEvent.change(deploymentName, { target: { value: "d" } });
|
||||||
fireEvent.change(modelName, { target: { value: "m" } });
|
fireEvent.change(modelName, { target: { value: "m" } });
|
||||||
fireEvent.change(endpoint, { target: { value: "https://x.example.com" } });
|
fireEvent.change(endpoint, { target: { value: "https://x.openai.azure.com" } });
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1];
|
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1];
|
||||||
@@ -264,6 +288,102 @@ describe("VectorEmbeddingPoliciesComponent - embedding source", () => {
|
|||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
expect(onChange.mock.calls.length).toBe(stable);
|
expect(onChange.mock.calls.length).toBe(stable);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("model-specific dimension validation blocks out-of-range values", async () => {
|
||||||
|
expandSection();
|
||||||
|
fireEvent.change(view.container.querySelector("#vector-policy-dimension-1"), { target: { value: "3073" } });
|
||||||
|
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-sourcePaths-1"), {
|
||||||
|
target: { value: "/description" },
|
||||||
|
});
|
||||||
|
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-deploymentName-1"), {
|
||||||
|
target: { value: "text-embedding-3-large" },
|
||||||
|
});
|
||||||
|
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-modelName-1"), {
|
||||||
|
target: { value: "text-embedding-3-large" },
|
||||||
|
});
|
||||||
|
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-endpoint-1"), {
|
||||||
|
target: { value: "https://my-foundry.openai.azure.com" },
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Dimension must be greater than 0 and less than or equal 3072")).toBeInTheDocument();
|
||||||
|
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1];
|
||||||
|
expect(lastCall[2]).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("existing embedding source allows endpoint edit but keeps source fields read-only", async () => {
|
||||||
|
const existingEmbedding: VectorEmbedding[] = [
|
||||||
|
{
|
||||||
|
path: "/vector4",
|
||||||
|
dataType: "float32",
|
||||||
|
distanceFunction: "cosine",
|
||||||
|
dimensions: 1536,
|
||||||
|
embeddingSource: {
|
||||||
|
sourcePaths: ["/description"],
|
||||||
|
deploymentName: "text-embedding-3-small",
|
||||||
|
modelName: "text-embedding-3-small",
|
||||||
|
endpoint: "https://old.openai.azure.com",
|
||||||
|
authType: "Entra",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const existingOnChange = jest.fn();
|
||||||
|
const existingView = render(
|
||||||
|
<VectorEmbeddingPoliciesComponent
|
||||||
|
vectorEmbeddingsBaseline={existingEmbedding}
|
||||||
|
vectorEmbeddings={existingEmbedding}
|
||||||
|
vectorIndexes={[]}
|
||||||
|
onVectorEmbeddingChange={existingOnChange}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const sourcePaths = existingView.container.querySelector(
|
||||||
|
"#vector-policy-embeddingSource-sourcePaths-1",
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const endpoint = existingView.container.querySelector(
|
||||||
|
"#vector-policy-embeddingSource-endpoint-1",
|
||||||
|
) as HTMLInputElement;
|
||||||
|
expect(sourcePaths).toBeDisabled();
|
||||||
|
expect(endpoint).not.toBeDisabled();
|
||||||
|
|
||||||
|
fireEvent.change(endpoint, { target: { value: "https://new.openai.azure.com" } });
|
||||||
|
await waitFor(() => {
|
||||||
|
const lastCall = existingOnChange.mock.calls[existingOnChange.mock.calls.length - 1];
|
||||||
|
expect(lastCall[2]).toBe(true);
|
||||||
|
expect(lastCall[0][0].embeddingSource.endpoint).toBe("https://new.openai.azure.com");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("existing vector policy without an embedding source cannot add one", () => {
|
||||||
|
const existingEmbedding: VectorEmbedding[] = [
|
||||||
|
{
|
||||||
|
path: "/vector5",
|
||||||
|
dataType: "float32",
|
||||||
|
distanceFunction: "cosine",
|
||||||
|
dimensions: 1536,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const existingView = render(
|
||||||
|
<VectorEmbeddingPoliciesComponent
|
||||||
|
vectorEmbeddingsBaseline={existingEmbedding}
|
||||||
|
vectorEmbeddings={existingEmbedding}
|
||||||
|
vectorIndexes={[]}
|
||||||
|
onVectorEmbeddingChange={jest.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(existingView.container.querySelector('[data-test="VectorEmbeddingSource/Section/1"]'));
|
||||||
|
|
||||||
|
expect(existingView.container.querySelector("#vector-policy-embeddingSource-sourcePaths-1")).toBeDisabled();
|
||||||
|
expect(existingView.container.querySelector("#vector-policy-embeddingSource-deploymentName-1")).toBeDisabled();
|
||||||
|
expect(existingView.container.querySelector("#vector-policy-embeddingSource-modelName-1")).toBeDisabled();
|
||||||
|
expect(existingView.container.querySelector("#vector-policy-embeddingSource-authType-1")).toHaveAttribute(
|
||||||
|
"aria-disabled",
|
||||||
|
"true",
|
||||||
|
);
|
||||||
|
expect(existingView.container.querySelector("#vector-policy-embeddingSource-endpoint-1")).not.toBeDisabled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("VectorEmbeddingPoliciesComponent - embedding source gating", () => {
|
describe("VectorEmbeddingPoliciesComponent - embedding source gating", () => {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export interface VectorEmbeddingPolicyData {
|
|||||||
distanceFunction: VectorEmbedding["distanceFunction"];
|
distanceFunction: VectorEmbedding["distanceFunction"];
|
||||||
dimensions: number;
|
dimensions: number;
|
||||||
indexType: VectorIndex["type"] | "none";
|
indexType: VectorIndex["type"] | "none";
|
||||||
|
dataTypeError: string;
|
||||||
pathError: string;
|
pathError: string;
|
||||||
dimensionsError: string;
|
dimensionsError: string;
|
||||||
vectorIndexShardKey?: string[];
|
vectorIndexShardKey?: string[];
|
||||||
@@ -54,6 +55,18 @@ export interface VectorEmbeddingPolicyData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type VectorEmbeddingPolicyProperty = "dataType" | "distanceFunction" | "indexType";
|
type VectorEmbeddingPolicyProperty = "dataType" | "distanceFunction" | "indexType";
|
||||||
|
const embeddingSourceSupportedDataTypes: VectorEmbedding["dataType"][] = ["float32", "float16"];
|
||||||
|
|
||||||
|
const getEmbeddingSourceDimensionLimit = (modelName: string | undefined): number | undefined => {
|
||||||
|
switch (modelName?.trim()) {
|
||||||
|
case "text-embedding-3-large":
|
||||||
|
return 3072;
|
||||||
|
case "text-embedding-3-small":
|
||||||
|
return 1536;
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddingPoliciesComponentProps> = ({
|
export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddingPoliciesComponentProps> = ({
|
||||||
vectorEmbeddingsBaseline,
|
vectorEmbeddingsBaseline,
|
||||||
@@ -95,7 +108,21 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
|||||||
return error;
|
return error;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onVectorEmbeddingDimensionError = (dimension: number, indexType: VectorIndex["type"] | "none"): string => {
|
const onVectorEmbeddingDataTypeError = (
|
||||||
|
dataType: VectorEmbedding["dataType"],
|
||||||
|
embeddingSource?: VectorEmbeddingSource,
|
||||||
|
): string => {
|
||||||
|
if (embeddingSource && !embeddingSourceSupportedDataTypes.includes(dataType)) {
|
||||||
|
return t(Keys.controls.vectorEmbeddingPolicies.embeddingSourceDataTypeError);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const onVectorEmbeddingDimensionError = (
|
||||||
|
dimension: number,
|
||||||
|
indexType: VectorIndex["type"] | "none",
|
||||||
|
embeddingSource?: VectorEmbeddingSource,
|
||||||
|
): string => {
|
||||||
let error = "";
|
let error = "";
|
||||||
if (dimension <= 0 || dimension > 4096) {
|
if (dimension <= 0 || dimension > 4096) {
|
||||||
error = t(Keys.controls.vectorEmbeddingPolicies.dimensionRangeError);
|
error = t(Keys.controls.vectorEmbeddingPolicies.dimensionRangeError);
|
||||||
@@ -103,6 +130,15 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
|||||||
if (indexType === "flat" && dimension > 505) {
|
if (indexType === "flat" && dimension > 505) {
|
||||||
error = t(Keys.controls.vectorEmbeddingPolicies.dimensionFlatIndexError);
|
error = t(Keys.controls.vectorEmbeddingPolicies.dimensionFlatIndexError);
|
||||||
}
|
}
|
||||||
|
if (embeddingSource?.modelName === "text-embedding-ada-002" && dimension !== 1536) {
|
||||||
|
error = t(Keys.controls.vectorEmbeddingPolicies.adaDimensionError);
|
||||||
|
}
|
||||||
|
const modelDimensionLimit = getEmbeddingSourceDimensionLimit(embeddingSource?.modelName);
|
||||||
|
if (modelDimensionLimit && (dimension <= 0 || dimension > modelDimensionLimit)) {
|
||||||
|
error = t(Keys.controls.vectorEmbeddingPolicies.modelDimensionRangeError, {
|
||||||
|
max: modelDimensionLimit,
|
||||||
|
});
|
||||||
|
}
|
||||||
return error;
|
return error;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -137,7 +173,12 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
|||||||
quantizerType: supportsQuantizer ? matchingIndex?.quantizerType || "product" : undefined,
|
quantizerType: supportsQuantizer ? matchingIndex?.quantizerType || "product" : undefined,
|
||||||
vectorIndexShardKey: matchingIndex?.vectorIndexShardKey || undefined,
|
vectorIndexShardKey: matchingIndex?.vectorIndexShardKey || undefined,
|
||||||
pathError: onVectorEmbeddingPathError(embedding.path),
|
pathError: onVectorEmbeddingPathError(embedding.path),
|
||||||
dimensionsError: onVectorEmbeddingDimensionError(embedding.dimensions, matchingIndex?.type || "none"),
|
dataTypeError: onVectorEmbeddingDataTypeError(embedding.dataType, embedding.embeddingSource),
|
||||||
|
dimensionsError: onVectorEmbeddingDimensionError(
|
||||||
|
embedding.dimensions,
|
||||||
|
matchingIndex?.type || "none",
|
||||||
|
embedding.embeddingSource,
|
||||||
|
),
|
||||||
embeddingSource: embedding.embeddingSource,
|
embeddingSource: embedding.embeddingSource,
|
||||||
embeddingSourceValid: true,
|
embeddingSourceValid: true,
|
||||||
});
|
});
|
||||||
@@ -192,7 +233,10 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
|||||||
);
|
);
|
||||||
const validationPassed = vectorEmbeddingPolicyData.every(
|
const validationPassed = vectorEmbeddingPolicyData.every(
|
||||||
(policy: VectorEmbeddingPolicyData) =>
|
(policy: VectorEmbeddingPolicyData) =>
|
||||||
policy.pathError === "" && policy.dimensionsError === "" && policy.embeddingSourceValid,
|
policy.pathError === "" &&
|
||||||
|
policy.dataTypeError === "" &&
|
||||||
|
policy.dimensionsError === "" &&
|
||||||
|
policy.embeddingSourceValid,
|
||||||
);
|
);
|
||||||
|
|
||||||
onVectorEmbeddingChange(vectorEmbeddings, vectorIndexes, validationPassed);
|
onVectorEmbeddingChange(vectorEmbeddings, vectorIndexes, validationPassed);
|
||||||
@@ -216,7 +260,7 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
|||||||
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||||
const vectorEmbedding = vectorEmbeddings[index];
|
const vectorEmbedding = vectorEmbeddings[index];
|
||||||
vectorEmbeddings[index].dimensions = value;
|
vectorEmbeddings[index].dimensions = value;
|
||||||
const error = onVectorEmbeddingDimensionError(value, vectorEmbedding.indexType);
|
const error = onVectorEmbeddingDimensionError(value, vectorEmbedding.indexType, vectorEmbedding.embeddingSource);
|
||||||
vectorEmbeddings[index].dimensionsError = error;
|
vectorEmbeddings[index].dimensionsError = error;
|
||||||
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||||
};
|
};
|
||||||
@@ -225,7 +269,11 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
|||||||
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||||
const vectorEmbedding = vectorEmbeddings[index];
|
const vectorEmbedding = vectorEmbeddings[index];
|
||||||
vectorEmbeddings[index].indexType = option.key as never;
|
vectorEmbeddings[index].indexType = option.key as never;
|
||||||
const error = onVectorEmbeddingDimensionError(vectorEmbedding.dimensions, vectorEmbedding.indexType);
|
const error = onVectorEmbeddingDimensionError(
|
||||||
|
vectorEmbedding.dimensions,
|
||||||
|
vectorEmbedding.indexType,
|
||||||
|
vectorEmbedding.embeddingSource,
|
||||||
|
);
|
||||||
vectorEmbeddings[index].dimensionsError = error;
|
vectorEmbeddings[index].dimensionsError = error;
|
||||||
if (vectorEmbedding.indexType === "diskANN") {
|
if (vectorEmbedding.indexType === "diskANN") {
|
||||||
vectorEmbedding.indexingSearchListSize = 100;
|
vectorEmbedding.indexingSearchListSize = 100;
|
||||||
@@ -280,6 +328,12 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
|||||||
): void => {
|
): void => {
|
||||||
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||||
vectorEmbeddings[index][property] = option.key as never;
|
vectorEmbeddings[index][property] = option.key as never;
|
||||||
|
if (property === "dataType") {
|
||||||
|
vectorEmbeddings[index].dataTypeError = onVectorEmbeddingDataTypeError(
|
||||||
|
vectorEmbeddings[index].dataType,
|
||||||
|
vectorEmbeddings[index].embeddingSource,
|
||||||
|
);
|
||||||
|
}
|
||||||
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -294,7 +348,15 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
|||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
const next = [...prev];
|
const next = [...prev];
|
||||||
next[index] = { ...current, embeddingSource, embeddingSourceValid: isValid };
|
const dataType = embeddingSourceSupportedDataTypes.includes(current.dataType) ? current.dataType : "float32";
|
||||||
|
next[index] = {
|
||||||
|
...current,
|
||||||
|
dataType,
|
||||||
|
dataTypeError: onVectorEmbeddingDataTypeError(dataType, embeddingSource),
|
||||||
|
dimensionsError: onVectorEmbeddingDimensionError(current.dimensions, current.indexType, embeddingSource),
|
||||||
|
embeddingSource,
|
||||||
|
embeddingSourceValid: isValid,
|
||||||
|
};
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -311,6 +373,7 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
|||||||
distanceFunction: "euclidean",
|
distanceFunction: "euclidean",
|
||||||
dimensions: 0,
|
dimensions: 0,
|
||||||
indexType: "none",
|
indexType: "none",
|
||||||
|
dataTypeError: "",
|
||||||
pathError: onVectorEmbeddingPathError(""),
|
pathError: onVectorEmbeddingPathError(""),
|
||||||
dimensionsError: onVectorEmbeddingDimensionError(0, "none"),
|
dimensionsError: onVectorEmbeddingDimensionError(0, "none"),
|
||||||
embeddingSource: undefined,
|
embeddingSource: undefined,
|
||||||
@@ -381,11 +444,12 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
|||||||
disabled={isExistingPolicy(vectorEmbeddingPolicy)}
|
disabled={isExistingPolicy(vectorEmbeddingPolicy)}
|
||||||
required={true}
|
required={true}
|
||||||
styles={dropdownStyles}
|
styles={dropdownStyles}
|
||||||
options={getDataTypeOptions()}
|
options={getDataTypeOptions(!!vectorEmbeddingPolicy.embeddingSource)}
|
||||||
selectedKey={vectorEmbeddingPolicy.dataType}
|
selectedKey={vectorEmbeddingPolicy.dataType}
|
||||||
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||||
onVectorEmbeddingPolicyChange(index, option, "dataType")
|
onVectorEmbeddingPolicyChange(index, option, "dataType")
|
||||||
}
|
}
|
||||||
|
errorMessage={vectorEmbeddingPolicy.dataTypeError}
|
||||||
></Dropdown>
|
></Dropdown>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
@@ -529,6 +593,7 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
|||||||
{isIntegratedEmbeddingEnabled() && (
|
{isIntegratedEmbeddingEnabled() && (
|
||||||
<VectorEmbeddingSourceComponent
|
<VectorEmbeddingSourceComponent
|
||||||
index={index}
|
index={index}
|
||||||
|
vectorPath={vectorEmbeddingPolicy.path}
|
||||||
disabled={isExistingPolicy(vectorEmbeddingPolicy)}
|
disabled={isExistingPolicy(vectorEmbeddingPolicy)}
|
||||||
initialEmbeddingSource={vectorEmbeddingPolicy.embeddingSource}
|
initialEmbeddingSource={vectorEmbeddingPolicy.embeddingSource}
|
||||||
discardChanges={discardChanges}
|
discardChanges={discardChanges}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/
|
|||||||
import { VectorEmbeddingSource } from "Contracts/DataModels";
|
import { VectorEmbeddingSource } from "Contracts/DataModels";
|
||||||
import {
|
import {
|
||||||
getAuthTypeOptions,
|
getAuthTypeOptions,
|
||||||
isValidHttpsUrl,
|
isValidFoundryEndpoint,
|
||||||
parseSourcePaths,
|
parseSourcePaths,
|
||||||
} from "Explorer/Controls/VectorSearch/VectorSearchUtils";
|
} from "Explorer/Controls/VectorSearch/VectorSearchUtils";
|
||||||
import { dropdownStyles, labelStyles, textFieldStyles } from "Explorer/Controls/VectorSearch/vectorSearchStyles";
|
import { dropdownStyles, labelStyles, textFieldStyles } from "Explorer/Controls/VectorSearch/vectorSearchStyles";
|
||||||
@@ -13,6 +13,7 @@ import React, { FunctionComponent, useState } from "react";
|
|||||||
|
|
||||||
export interface IVectorEmbeddingSourceComponentProps {
|
export interface IVectorEmbeddingSourceComponentProps {
|
||||||
index: number;
|
index: number;
|
||||||
|
vectorPath: string;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
initialEmbeddingSource?: VectorEmbeddingSource;
|
initialEmbeddingSource?: VectorEmbeddingSource;
|
||||||
discardChanges?: boolean;
|
discardChanges?: boolean;
|
||||||
@@ -38,13 +39,19 @@ const EmbeddingSourceLabel = ({ disabled, label, tooltip }: EmbeddingSourceLabel
|
|||||||
</Label>
|
</Label>
|
||||||
);
|
);
|
||||||
|
|
||||||
const validateSourcePaths = (raw: string): string => {
|
const validateSourcePaths = (raw: string, vectorPath: string): string => {
|
||||||
const parsed = parseSourcePaths(raw);
|
const parsed = parseSourcePaths(raw);
|
||||||
if (parsed.length === 0) {
|
if (parsed.length === 0) {
|
||||||
return t(Keys.controls.vectorEmbeddingPolicies.sourcePathsRequiredError);
|
return t(Keys.controls.vectorEmbeddingPolicies.sourcePathsRequiredError);
|
||||||
}
|
}
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
for (const p of parsed) {
|
for (const p of parsed) {
|
||||||
|
if (!p.startsWith("/")) {
|
||||||
|
return t(Keys.controls.vectorEmbeddingPolicies.sourcePathInvalidError);
|
||||||
|
}
|
||||||
|
if (p === vectorPath) {
|
||||||
|
return t(Keys.controls.vectorEmbeddingPolicies.sourcePathSameAsVectorPathError);
|
||||||
|
}
|
||||||
if (seen.has(p)) {
|
if (seen.has(p)) {
|
||||||
return t(Keys.controls.vectorEmbeddingPolicies.sourcePathDuplicateError);
|
return t(Keys.controls.vectorEmbeddingPolicies.sourcePathDuplicateError);
|
||||||
}
|
}
|
||||||
@@ -64,7 +71,7 @@ const validateEndpoint = (value: string | undefined): string => {
|
|||||||
if (!value || value.trim().length === 0) {
|
if (!value || value.trim().length === 0) {
|
||||||
return t(Keys.controls.vectorEmbeddingPolicies.endpointRequiredError);
|
return t(Keys.controls.vectorEmbeddingPolicies.endpointRequiredError);
|
||||||
}
|
}
|
||||||
if (!isValidHttpsUrl(value.trim())) {
|
if (!isValidFoundryEndpoint(value.trim())) {
|
||||||
return t(Keys.controls.vectorEmbeddingPolicies.endpointInvalidError);
|
return t(Keys.controls.vectorEmbeddingPolicies.endpointInvalidError);
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
@@ -72,6 +79,7 @@ const validateEndpoint = (value: string | undefined): string => {
|
|||||||
|
|
||||||
export const VectorEmbeddingSourceComponent: FunctionComponent<IVectorEmbeddingSourceComponentProps> = ({
|
export const VectorEmbeddingSourceComponent: FunctionComponent<IVectorEmbeddingSourceComponentProps> = ({
|
||||||
index,
|
index,
|
||||||
|
vectorPath,
|
||||||
disabled,
|
disabled,
|
||||||
initialEmbeddingSource,
|
initialEmbeddingSource,
|
||||||
discardChanges,
|
discardChanges,
|
||||||
@@ -93,7 +101,7 @@ export const VectorEmbeddingSourceComponent: FunctionComponent<IVectorEmbeddingS
|
|||||||
modelName.trim().length > 0 ||
|
modelName.trim().length > 0 ||
|
||||||
endpoint.trim().length > 0;
|
endpoint.trim().length > 0;
|
||||||
|
|
||||||
const sourcePathsError = hasAnyValue ? validateSourcePaths(sourcePathsRaw) : "";
|
const sourcePathsError = hasAnyValue ? validateSourcePaths(sourcePathsRaw, vectorPath) : "";
|
||||||
const deploymentNameError = hasAnyValue
|
const deploymentNameError = hasAnyValue
|
||||||
? validateRequired(deploymentName, Keys.controls.vectorEmbeddingPolicies.deploymentNameRequiredError)
|
? validateRequired(deploymentName, Keys.controls.vectorEmbeddingPolicies.deploymentNameRequiredError)
|
||||||
: "";
|
: "";
|
||||||
@@ -196,11 +204,11 @@ export const VectorEmbeddingSourceComponent: FunctionComponent<IVectorEmbeddingS
|
|||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Label disabled={disabled} styles={labelStyles}>
|
<Label disabled={false} styles={labelStyles}>
|
||||||
{t(Keys.controls.vectorEmbeddingPolicies.endpoint)}
|
{t(Keys.controls.vectorEmbeddingPolicies.endpoint)}
|
||||||
</Label>
|
</Label>
|
||||||
<TextField
|
<TextField
|
||||||
disabled={disabled}
|
disabled={false}
|
||||||
id={`vector-policy-embeddingSource-endpoint-${suffix}`}
|
id={`vector-policy-embeddingSource-endpoint-${suffix}`}
|
||||||
data-test={`VectorEmbeddingSource/Endpoint/${suffix}`}
|
data-test={`VectorEmbeddingSource/Endpoint/${suffix}`}
|
||||||
placeholder={t(Keys.controls.vectorEmbeddingPolicies.endpointPlaceholder)}
|
placeholder={t(Keys.controls.vectorEmbeddingPolicies.endpointPlaceholder)}
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ import { VectorEmbeddingSource, VectorIndex } from "Contracts/DataModels";
|
|||||||
import { Keys, t } from "Localization";
|
import { Keys, t } from "Localization";
|
||||||
|
|
||||||
const dataTypes = ["float32", "uint8", "int8", "float16"];
|
const dataTypes = ["float32", "uint8", "int8", "float16"];
|
||||||
|
const embeddingSourceDataTypes = ["float32", "float16"];
|
||||||
const distanceFunctions = ["euclidean", "cosine", "dotproduct"];
|
const distanceFunctions = ["euclidean", "cosine", "dotproduct"];
|
||||||
const indexTypes = ["none", "flat", "diskANN", "quantizedFlat"];
|
const indexTypes = ["none", "flat", "diskANN", "quantizedFlat"];
|
||||||
const authTypes: VectorEmbeddingSource["authType"][] = ["Entra"];
|
const authTypes: VectorEmbeddingSource["authType"][] = ["Entra"];
|
||||||
|
|
||||||
export const getDataTypeOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(dataTypes);
|
export const getDataTypeOptions = (hasEmbeddingSource = false): IDropdownOption[] =>
|
||||||
|
createDropdownOptionsFromLiterals(hasEmbeddingSource ? embeddingSourceDataTypes : dataTypes);
|
||||||
export const getDistanceFunctionOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(distanceFunctions);
|
export const getDistanceFunctionOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(distanceFunctions);
|
||||||
export const getIndexTypeOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(indexTypes);
|
export const getIndexTypeOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(indexTypes);
|
||||||
export const getAuthTypeOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(authTypes);
|
export const getAuthTypeOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(authTypes);
|
||||||
@@ -26,14 +28,14 @@ export const parseSourcePaths = (raw: string): string[] => {
|
|||||||
return raw
|
return raw
|
||||||
.split(",")
|
.split(",")
|
||||||
.map((p) => p.trim())
|
.map((p) => p.trim())
|
||||||
.filter((p) => p.length > 0)
|
.filter((p) => p.length > 0);
|
||||||
.map((p) => (p.startsWith("/") ? p : `/${p}`));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isValidHttpsUrl = (value: string): boolean => {
|
export const isValidFoundryEndpoint = (value: string): boolean => {
|
||||||
try {
|
try {
|
||||||
const url = new URL(value);
|
const url = new URL(value);
|
||||||
return url.protocol === "https:";
|
const allowedHostSuffixes = [".openai.azure.com", ".openai.azure.us", ".openai.azure.cn", ".services.ai.azure.com"];
|
||||||
|
return url.protocol === "https:" && allowedHostSuffixes.some((suffix) => url.hostname.endsWith(suffix));
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1014,14 +1014,19 @@
|
|||||||
"pathDuplicateError": "Path is already defined",
|
"pathDuplicateError": "Path is already defined",
|
||||||
"dimensionRangeError": "Dimension must be greater than 0 and less than or equal 4096",
|
"dimensionRangeError": "Dimension must be greater than 0 and less than or equal 4096",
|
||||||
"dimensionFlatIndexError": "Maximum allowed dimension for flat index is 505",
|
"dimensionFlatIndexError": "Maximum allowed dimension for flat index is 505",
|
||||||
|
"modelDimensionRangeError": "Dimension must be greater than 0 and less than or equal {{max}}",
|
||||||
|
"adaDimensionError": "Dimensions must be 1536",
|
||||||
"quantizationByteSizeRangeError": "Quantization byte size must be greater than 0 and less than or equal to 512",
|
"quantizationByteSizeRangeError": "Quantization byte size must be greater than 0 and less than or equal to 512",
|
||||||
"indexingSearchListSizeRangeError": "Indexing search list size must be greater than or equal to 25 and less than or equal to 500",
|
"indexingSearchListSizeRangeError": "Indexing search list size must be greater than or equal to 25 and less than or equal to 500",
|
||||||
"sourcePathsRequiredError": "At least one source path is required",
|
"sourcePathsRequiredError": "At least one source path is required",
|
||||||
|
"sourcePathInvalidError": "Source paths must start with /",
|
||||||
"sourcePathDuplicateError": "Source paths must be unique",
|
"sourcePathDuplicateError": "Source paths must be unique",
|
||||||
|
"sourcePathSameAsVectorPathError": "Source path must be different from vector path",
|
||||||
"deploymentNameRequiredError": "Deployment name is required",
|
"deploymentNameRequiredError": "Deployment name is required",
|
||||||
"modelNameRequiredError": "Model name is required",
|
"modelNameRequiredError": "Embedding model name is required",
|
||||||
"endpointRequiredError": "Endpoint is required",
|
"endpointRequiredError": "Microsoft Foundry Endpoint is required",
|
||||||
"endpointInvalidError": "Endpoint must be a valid https:// URL"
|
"endpointInvalidError": "Endpoint must be a valid Azure OpenAI or Foundry https:// URL",
|
||||||
|
"embeddingSourceDataTypeError": "Embedding generation supports only float32 or float16 data types"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"containerCopy": {
|
"containerCopy": {
|
||||||
|
|||||||
Reference in New Issue
Block a user