Add embedding source validation rules

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Archie Agarwal
2026-09-13 22:51:35 +05:30
parent d4fd0150e9
commit af7aa16ee7
5 changed files with 228 additions and 28 deletions
@@ -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;
} }
+8 -3
View File
@@ -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": {