mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-09-19 17:12:47 +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(() => {
|
||||
expect(screen.getByText("At least one source path is required")).toBeInTheDocument();
|
||||
expect(screen.getByText("Model name is required")).toBeInTheDocument();
|
||||
expect(screen.getByText("Endpoint is required")).toBeInTheDocument();
|
||||
expect(screen.getByText("Embedding model name is required")).toBeInTheDocument();
|
||||
expect(screen.getByText("Microsoft Foundry Endpoint is required")).toBeInTheDocument();
|
||||
});
|
||||
const last = onChange.mock.calls[onChange.mock.calls.length - 1];
|
||||
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();
|
||||
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-endpoint-1"), {
|
||||
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"), {
|
||||
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 () => {
|
||||
expandSection();
|
||||
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"), {
|
||||
target: { value: "my-deployment" },
|
||||
@@ -213,7 +237,7 @@ describe("VectorEmbeddingPoliciesComponent - embedding source", () => {
|
||||
fireEvent.change(sourcePaths, { target: { value: "/description" } });
|
||||
fireEvent.change(deploymentName, { target: { value: "d" } });
|
||||
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(() => {
|
||||
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));
|
||||
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", () => {
|
||||
|
||||
@@ -41,6 +41,7 @@ export interface VectorEmbeddingPolicyData {
|
||||
distanceFunction: VectorEmbedding["distanceFunction"];
|
||||
dimensions: number;
|
||||
indexType: VectorIndex["type"] | "none";
|
||||
dataTypeError: string;
|
||||
pathError: string;
|
||||
dimensionsError: string;
|
||||
vectorIndexShardKey?: string[];
|
||||
@@ -54,6 +55,18 @@ export interface VectorEmbeddingPolicyData {
|
||||
}
|
||||
|
||||
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> = ({
|
||||
vectorEmbeddingsBaseline,
|
||||
@@ -95,7 +108,21 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
||||
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 = "";
|
||||
if (dimension <= 0 || dimension > 4096) {
|
||||
error = t(Keys.controls.vectorEmbeddingPolicies.dimensionRangeError);
|
||||
@@ -103,6 +130,15 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
||||
if (indexType === "flat" && dimension > 505) {
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -137,7 +173,12 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
||||
quantizerType: supportsQuantizer ? matchingIndex?.quantizerType || "product" : undefined,
|
||||
vectorIndexShardKey: matchingIndex?.vectorIndexShardKey || undefined,
|
||||
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,
|
||||
embeddingSourceValid: true,
|
||||
});
|
||||
@@ -192,7 +233,10 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
||||
);
|
||||
const validationPassed = vectorEmbeddingPolicyData.every(
|
||||
(policy: VectorEmbeddingPolicyData) =>
|
||||
policy.pathError === "" && policy.dimensionsError === "" && policy.embeddingSourceValid,
|
||||
policy.pathError === "" &&
|
||||
policy.dataTypeError === "" &&
|
||||
policy.dimensionsError === "" &&
|
||||
policy.embeddingSourceValid,
|
||||
);
|
||||
|
||||
onVectorEmbeddingChange(vectorEmbeddings, vectorIndexes, validationPassed);
|
||||
@@ -216,7 +260,7 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
||||
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||
const vectorEmbedding = vectorEmbeddings[index];
|
||||
vectorEmbeddings[index].dimensions = value;
|
||||
const error = onVectorEmbeddingDimensionError(value, vectorEmbedding.indexType);
|
||||
const error = onVectorEmbeddingDimensionError(value, vectorEmbedding.indexType, vectorEmbedding.embeddingSource);
|
||||
vectorEmbeddings[index].dimensionsError = error;
|
||||
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||
};
|
||||
@@ -225,7 +269,11 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
||||
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||
const vectorEmbedding = vectorEmbeddings[index];
|
||||
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;
|
||||
if (vectorEmbedding.indexType === "diskANN") {
|
||||
vectorEmbedding.indexingSearchListSize = 100;
|
||||
@@ -280,6 +328,12 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
||||
): void => {
|
||||
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||
vectorEmbeddings[index][property] = option.key as never;
|
||||
if (property === "dataType") {
|
||||
vectorEmbeddings[index].dataTypeError = onVectorEmbeddingDataTypeError(
|
||||
vectorEmbeddings[index].dataType,
|
||||
vectorEmbeddings[index].embeddingSource,
|
||||
);
|
||||
}
|
||||
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||
};
|
||||
|
||||
@@ -294,7 +348,15 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
||||
return 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;
|
||||
});
|
||||
},
|
||||
@@ -311,6 +373,7 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
||||
distanceFunction: "euclidean",
|
||||
dimensions: 0,
|
||||
indexType: "none",
|
||||
dataTypeError: "",
|
||||
pathError: onVectorEmbeddingPathError(""),
|
||||
dimensionsError: onVectorEmbeddingDimensionError(0, "none"),
|
||||
embeddingSource: undefined,
|
||||
@@ -381,11 +444,12 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
||||
disabled={isExistingPolicy(vectorEmbeddingPolicy)}
|
||||
required={true}
|
||||
styles={dropdownStyles}
|
||||
options={getDataTypeOptions()}
|
||||
options={getDataTypeOptions(!!vectorEmbeddingPolicy.embeddingSource)}
|
||||
selectedKey={vectorEmbeddingPolicy.dataType}
|
||||
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||
onVectorEmbeddingPolicyChange(index, option, "dataType")
|
||||
}
|
||||
errorMessage={vectorEmbeddingPolicy.dataTypeError}
|
||||
></Dropdown>
|
||||
</Stack>
|
||||
<Stack>
|
||||
@@ -529,6 +593,7 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
||||
{isIntegratedEmbeddingEnabled() && (
|
||||
<VectorEmbeddingSourceComponent
|
||||
index={index}
|
||||
vectorPath={vectorEmbeddingPolicy.path}
|
||||
disabled={isExistingPolicy(vectorEmbeddingPolicy)}
|
||||
initialEmbeddingSource={vectorEmbeddingPolicy.embeddingSource}
|
||||
discardChanges={discardChanges}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/
|
||||
import { VectorEmbeddingSource } from "Contracts/DataModels";
|
||||
import {
|
||||
getAuthTypeOptions,
|
||||
isValidHttpsUrl,
|
||||
isValidFoundryEndpoint,
|
||||
parseSourcePaths,
|
||||
} from "Explorer/Controls/VectorSearch/VectorSearchUtils";
|
||||
import { dropdownStyles, labelStyles, textFieldStyles } from "Explorer/Controls/VectorSearch/vectorSearchStyles";
|
||||
@@ -13,6 +13,7 @@ import React, { FunctionComponent, useState } from "react";
|
||||
|
||||
export interface IVectorEmbeddingSourceComponentProps {
|
||||
index: number;
|
||||
vectorPath: string;
|
||||
disabled: boolean;
|
||||
initialEmbeddingSource?: VectorEmbeddingSource;
|
||||
discardChanges?: boolean;
|
||||
@@ -38,13 +39,19 @@ const EmbeddingSourceLabel = ({ disabled, label, tooltip }: EmbeddingSourceLabel
|
||||
</Label>
|
||||
);
|
||||
|
||||
const validateSourcePaths = (raw: string): string => {
|
||||
const validateSourcePaths = (raw: string, vectorPath: string): string => {
|
||||
const parsed = parseSourcePaths(raw);
|
||||
if (parsed.length === 0) {
|
||||
return t(Keys.controls.vectorEmbeddingPolicies.sourcePathsRequiredError);
|
||||
}
|
||||
const seen = new Set<string>();
|
||||
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)) {
|
||||
return t(Keys.controls.vectorEmbeddingPolicies.sourcePathDuplicateError);
|
||||
}
|
||||
@@ -64,7 +71,7 @@ const validateEndpoint = (value: string | undefined): string => {
|
||||
if (!value || value.trim().length === 0) {
|
||||
return t(Keys.controls.vectorEmbeddingPolicies.endpointRequiredError);
|
||||
}
|
||||
if (!isValidHttpsUrl(value.trim())) {
|
||||
if (!isValidFoundryEndpoint(value.trim())) {
|
||||
return t(Keys.controls.vectorEmbeddingPolicies.endpointInvalidError);
|
||||
}
|
||||
return "";
|
||||
@@ -72,6 +79,7 @@ const validateEndpoint = (value: string | undefined): string => {
|
||||
|
||||
export const VectorEmbeddingSourceComponent: FunctionComponent<IVectorEmbeddingSourceComponentProps> = ({
|
||||
index,
|
||||
vectorPath,
|
||||
disabled,
|
||||
initialEmbeddingSource,
|
||||
discardChanges,
|
||||
@@ -93,7 +101,7 @@ export const VectorEmbeddingSourceComponent: FunctionComponent<IVectorEmbeddingS
|
||||
modelName.trim().length > 0 ||
|
||||
endpoint.trim().length > 0;
|
||||
|
||||
const sourcePathsError = hasAnyValue ? validateSourcePaths(sourcePathsRaw) : "";
|
||||
const sourcePathsError = hasAnyValue ? validateSourcePaths(sourcePathsRaw, vectorPath) : "";
|
||||
const deploymentNameError = hasAnyValue
|
||||
? validateRequired(deploymentName, Keys.controls.vectorEmbeddingPolicies.deploymentNameRequiredError)
|
||||
: "";
|
||||
@@ -196,11 +204,11 @@ export const VectorEmbeddingSourceComponent: FunctionComponent<IVectorEmbeddingS
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Label disabled={disabled} styles={labelStyles}>
|
||||
<Label disabled={false} styles={labelStyles}>
|
||||
{t(Keys.controls.vectorEmbeddingPolicies.endpoint)}
|
||||
</Label>
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
disabled={false}
|
||||
id={`vector-policy-embeddingSource-endpoint-${suffix}`}
|
||||
data-test={`VectorEmbeddingSource/Endpoint/${suffix}`}
|
||||
placeholder={t(Keys.controls.vectorEmbeddingPolicies.endpointPlaceholder)}
|
||||
|
||||
@@ -3,11 +3,13 @@ import { VectorEmbeddingSource, VectorIndex } from "Contracts/DataModels";
|
||||
import { Keys, t } from "Localization";
|
||||
|
||||
const dataTypes = ["float32", "uint8", "int8", "float16"];
|
||||
const embeddingSourceDataTypes = ["float32", "float16"];
|
||||
const distanceFunctions = ["euclidean", "cosine", "dotproduct"];
|
||||
const indexTypes = ["none", "flat", "diskANN", "quantizedFlat"];
|
||||
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 getIndexTypeOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(indexTypes);
|
||||
export const getAuthTypeOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(authTypes);
|
||||
@@ -26,14 +28,14 @@ export const parseSourcePaths = (raw: string): string[] => {
|
||||
return raw
|
||||
.split(",")
|
||||
.map((p) => p.trim())
|
||||
.filter((p) => p.length > 0)
|
||||
.map((p) => (p.startsWith("/") ? p : `/${p}`));
|
||||
.filter((p) => p.length > 0);
|
||||
};
|
||||
|
||||
export const isValidHttpsUrl = (value: string): boolean => {
|
||||
export const isValidFoundryEndpoint = (value: string): boolean => {
|
||||
try {
|
||||
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 {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1014,14 +1014,19 @@
|
||||
"pathDuplicateError": "Path is already defined",
|
||||
"dimensionRangeError": "Dimension must be greater than 0 and less than or equal 4096",
|
||||
"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",
|
||||
"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",
|
||||
"sourcePathInvalidError": "Source paths must start with /",
|
||||
"sourcePathDuplicateError": "Source paths must be unique",
|
||||
"sourcePathSameAsVectorPathError": "Source path must be different from vector path",
|
||||
"deploymentNameRequiredError": "Deployment name is required",
|
||||
"modelNameRequiredError": "Model name is required",
|
||||
"endpointRequiredError": "Endpoint is required",
|
||||
"endpointInvalidError": "Endpoint must be a valid https:// URL"
|
||||
"modelNameRequiredError": "Embedding model name is required",
|
||||
"endpointRequiredError": "Microsoft Foundry Endpoint is required",
|
||||
"endpointInvalidError": "Endpoint must be a valid Azure OpenAI or Foundry https:// URL",
|
||||
"embeddingSourceDataTypeError": "Embedding generation supports only float32 or float16 data types"
|
||||
}
|
||||
},
|
||||
"containerCopy": {
|
||||
|
||||
Reference in New Issue
Block a user