init ui for bedrock s3 vectors

This commit is contained in:
Ishaan Jaffer 2026-01-27 14:53:29 -08:00
parent 7aa11ecf4a
commit fc4a484cb1
6 changed files with 611 additions and 5 deletions

View file

@ -6954,7 +6954,8 @@ export const ragIngestCall = async (
customLlmProvider: string,
vectorStoreId?: string,
vectorStoreName?: string,
vectorStoreDescription?: string
vectorStoreDescription?: string,
providerSpecificParams?: Record<string, any>
): Promise<any> => {
try {
let url = proxyBaseUrl ? `${proxyBaseUrl}/rag/ingest` : `/rag/ingest`;
@ -6967,6 +6968,7 @@ export const ragIngestCall = async (
vector_store: {
custom_llm_provider: customLlmProvider,
...(vectorStoreId && { vector_store_id: vectorStoreId }),
...(providerSpecificParams && providerSpecificParams),
},
},
};

View file

@ -22,17 +22,51 @@ vi.mock("../vector_store_providers", () => ({
BEDROCK: "Amazon Bedrock",
OPENAI: "OpenAI",
AZURE_OPENAI: "Azure OpenAI",
S3Vectors: "AWS S3 Vectors",
},
vectorStoreProviderMap: {
BEDROCK: "bedrock",
OPENAI: "openai",
AZURE_OPENAI: "azure_openai",
S3Vectors: "s3_vectors",
},
vectorStoreProviderLogoMap: {
"Amazon Bedrock": "https://example.com/bedrock.png",
"OpenAI": "https://example.com/openai.png",
"Azure OpenAI": "https://example.com/azure.png",
"AWS S3 Vectors": "https://example.com/aws.png",
},
getProviderSpecificFields: vi.fn((provider: string) => {
if (provider === "s3_vectors") {
return [
{
name: "vector_bucket_name",
label: "Vector Bucket Name",
tooltip: "S3 bucket name for vector storage",
placeholder: "my-vector-bucket",
required: true,
type: "text",
},
{
name: "aws_region_name",
label: "AWS Region",
tooltip: "AWS region",
placeholder: "us-west-2",
required: true,
type: "text",
},
{
name: "embedding_model",
label: "Embedding Model",
tooltip: "Embedding model to use",
placeholder: "text-embedding-3-small",
required: true,
type: "select",
},
];
}
return [];
}),
}));
describe("CreateVectorStore", () => {
@ -43,9 +77,9 @@ describe("CreateVectorStore", () => {
it("should render the component successfully", () => {
render(<CreateVectorStore accessToken="test-token" />);
expect(screen.getByText("Create Vector Store")).toBeInTheDocument();
expect(screen.getAllByText("Create Vector Store").length).toBeGreaterThan(0);
expect(screen.getByText("Step 1: Upload Documents")).toBeInTheDocument();
expect(screen.getByText("Step 2: Select Provider")).toBeInTheDocument();
expect(screen.getByText("Step 2: Configure Vector Store")).toBeInTheDocument();
});
it("should display upload area with correct text", () => {
@ -123,7 +157,15 @@ describe("CreateVectorStore", () => {
});
await waitFor(() => {
expect(mockRagIngestCall).toHaveBeenCalledWith("test-token", expect.any(File), "bedrock", undefined);
expect(mockRagIngestCall).toHaveBeenCalledWith(
"test-token",
expect.any(File),
"bedrock",
undefined,
undefined,
undefined,
{}
);
});
});
@ -163,4 +205,72 @@ describe("CreateVectorStore", () => {
expect(screen.getByText("Vector Store Created Successfully")).toBeInTheDocument();
});
});
it("should display S3 Vectors provider-specific fields when selected", async () => {
render(<CreateVectorStore accessToken="test-token" />);
// Find and click the provider dropdown
const providerSelect = screen.getByRole("combobox");
await act(async () => {
fireEvent.mouseDown(providerSelect);
});
// Wait for dropdown options to appear
await waitFor(() => {
const s3Option = screen.queryByText("AWS S3 Vectors");
if (s3Option) {
fireEvent.click(s3Option);
}
});
// Check if S3-specific fields are displayed
await waitFor(() => {
expect(screen.queryByText("Vector Bucket Name")).toBeInTheDocument();
expect(screen.queryByText("AWS Region")).toBeInTheDocument();
expect(screen.queryByText("Embedding Model")).toBeInTheDocument();
});
});
it("should validate S3 Vectors required fields before submission", async () => {
render(<CreateVectorStore accessToken="test-token" />);
// Upload a file first
const file = new File(["test content"], "test.pdf", { type: "application/pdf" });
const uploadInput = document.querySelector('input[type="file"]') as HTMLInputElement;
await act(async () => {
if (uploadInput) {
fireEvent.change(uploadInput, { target: { files: [file] } });
}
});
await waitFor(() => {
expect(screen.getByText("Uploaded Documents (1)")).toBeInTheDocument();
});
// Select S3 Vectors provider
const providerSelect = screen.getByRole("combobox");
await act(async () => {
fireEvent.mouseDown(providerSelect);
});
await waitFor(() => {
const s3Option = screen.queryByText("AWS S3 Vectors");
if (s3Option) {
fireEvent.click(s3Option);
}
});
// Try to create without filling required fields
const createButton = screen.getByRole("button", { name: /Create Vector Store/i });
await act(async () => {
fireEvent.click(createButton);
});
// Should show validation warning (mocked message.warning would be called)
// The actual validation happens in the component
});
});

View file

@ -10,8 +10,11 @@ import {
VectorStoreProviders,
vectorStoreProviderLogoMap,
vectorStoreProviderMap,
getProviderSpecificFields,
VectorStoreFieldConfig,
} from "../vector_store_providers";
import NotificationsManager from "../molecules/notifications_manager";
import S3VectorsConfig from "./S3VectorsConfig";
const { Dragger } = Upload;
@ -28,6 +31,7 @@ const CreateVectorStore: React.FC<CreateVectorStoreProps> = ({ accessToken, onSu
const [vectorStoreName, setVectorStoreName] = useState<string>("");
const [vectorStoreDescription, setVectorStoreDescription] = useState<string>("");
const [ingestResults, setIngestResults] = useState<RAGIngestResponse[]>([]);
const [providerParams, setProviderParams] = useState<Record<string, any>>({});
const uploadProps: UploadProps = {
name: "file",
@ -92,6 +96,15 @@ const CreateVectorStore: React.FC<CreateVectorStoreProps> = ({ accessToken, onSu
return;
}
// Validate provider-specific required fields
const requiredFields = getProviderSpecificFields(selectedProvider).filter((field) => field.required);
for (const field of requiredFields) {
if (!providerParams[field.name]) {
message.warning(`Please provide ${field.label}`);
return;
}
}
if (!accessToken) {
message.error("No access token available");
return;
@ -118,7 +131,8 @@ const CreateVectorStore: React.FC<CreateVectorStoreProps> = ({ accessToken, onSu
selectedProvider,
vectorStoreId, // Use the same vector store ID for subsequent uploads
vectorStoreName || undefined,
vectorStoreDescription || undefined
vectorStoreDescription || undefined,
providerParams
);
// Store the vector store ID from the first successful ingest
@ -298,6 +312,74 @@ const CreateVectorStore: React.FC<CreateVectorStoreProps> = ({ accessToken, onSu
})}
</Select>
</Form.Item>
{/* S3 Vectors Configuration */}
{selectedProvider === "s3_vectors" && (
<S3VectorsConfig
accessToken={accessToken}
providerParams={providerParams}
onParamsChange={setProviderParams}
/>
)}
{/* Other Provider-specific fields */}
{selectedProvider !== "s3_vectors" &&
getProviderSpecificFields(selectedProvider).map((field: VectorStoreFieldConfig) => {
if (field.type === "select") {
// For embedding model selection, we'd need to fetch available models
// For now, provide a text input as fallback
return (
<Form.Item
key={field.name}
label={
<span>
{field.label}{" "}
<Tooltip title={field.tooltip}>
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</Tooltip>
</span>
}
required={field.required}
>
<Input
value={providerParams[field.name] || ""}
onChange={(e) =>
setProviderParams((prev) => ({ ...prev, [field.name]: e.target.value }))
}
placeholder={field.placeholder}
size="large"
className="rounded-md"
/>
</Form.Item>
);
}
return (
<Form.Item
key={field.name}
label={
<span>
{field.label}{" "}
<Tooltip title={field.tooltip}>
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</Tooltip>
</span>
}
required={field.required}
>
<Input
type={field.type === "password" ? "password" : "text"}
value={providerParams[field.name] || ""}
onChange={(e) =>
setProviderParams((prev) => ({ ...prev, [field.name]: e.target.value }))
}
placeholder={field.placeholder}
size="large"
className="rounded-md"
/>
</Form.Item>
);
})}
</Form>
<div className="flex justify-end">

View file

@ -0,0 +1,203 @@
import { render, screen, fireEvent, waitFor, act } from "@testing-library/react";
import { describe, it, expect, vi, beforeEach } from "vitest";
import S3VectorsConfig from "./S3VectorsConfig";
import * as fetchModels from "../playground/llm_calls/fetch_models";
// Mock fetchAvailableModels
vi.mock("../playground/llm_calls/fetch_models", () => ({
fetchAvailableModels: vi.fn(),
}));
describe("S3VectorsConfig", () => {
const mockOnParamsChange = vi.fn();
const defaultProps = {
accessToken: "test-token",
providerParams: {},
onParamsChange: mockOnParamsChange,
};
beforeEach(() => {
vi.clearAllMocks();
});
it("should render the component successfully", () => {
vi.spyOn(fetchModels, "fetchAvailableModels").mockResolvedValue([]);
render(<S3VectorsConfig {...defaultProps} />);
expect(screen.getByText("AWS S3 Vectors Setup")).toBeInTheDocument();
expect(screen.getByText("Vector Bucket Name")).toBeInTheDocument();
expect(screen.getByText("Index Name")).toBeInTheDocument();
expect(screen.getByText("AWS Region")).toBeInTheDocument();
expect(screen.getByText("Embedding Model")).toBeInTheDocument();
});
it("should display setup instructions", () => {
vi.spyOn(fetchModels, "fetchAvailableModels").mockResolvedValue([]);
render(<S3VectorsConfig {...defaultProps} />);
expect(
screen.getByText(/AWS S3 Vectors allows you to store and query vector embeddings directly in S3/)
).toBeInTheDocument();
expect(screen.getByText(/Vector buckets and indexes will be automatically created/)).toBeInTheDocument();
expect(screen.getByText(/Vector dimensions are auto-detected/)).toBeInTheDocument();
});
it("should fetch embedding models on mount", async () => {
const mockModels = [
{ model_group: "text-embedding-3-small", mode: "embedding" },
{ model_group: "text-embedding-3-large", mode: "embedding" },
{ model_group: "gpt-4", mode: "chat" },
];
const fetchSpy = vi.spyOn(fetchModels, "fetchAvailableModels").mockResolvedValue(mockModels);
render(<S3VectorsConfig {...defaultProps} />);
await waitFor(() => {
expect(fetchSpy).toHaveBeenCalledWith("test-token");
});
});
it("should filter and display only embedding models", async () => {
const mockModels = [
{ model_group: "text-embedding-3-small", mode: "embedding" },
{ model_group: "text-embedding-3-large", mode: "embedding" },
{ model_group: "gpt-4", mode: "chat" },
{ model_group: "gpt-3.5-turbo", mode: "chat" },
];
vi.spyOn(fetchModels, "fetchAvailableModels").mockResolvedValue(mockModels);
render(<S3VectorsConfig {...defaultProps} />);
// Wait for models to load
await waitFor(() => {
expect(fetchModels.fetchAvailableModels).toHaveBeenCalled();
});
// The component should filter to only embedding models internally
// We can verify this by checking the component loaded successfully
expect(screen.getByText("Embedding Model")).toBeInTheDocument();
});
it("should call onParamsChange when vector bucket name changes", async () => {
vi.spyOn(fetchModels, "fetchAvailableModels").mockResolvedValue([]);
render(<S3VectorsConfig {...defaultProps} />);
const bucketInput = screen.getByPlaceholderText("my-vector-bucket");
await act(async () => {
fireEvent.change(bucketInput, { target: { value: "test-bucket" } });
});
expect(mockOnParamsChange).toHaveBeenCalledWith({
vector_bucket_name: "test-bucket",
});
});
it("should call onParamsChange when AWS region changes", async () => {
vi.spyOn(fetchModels, "fetchAvailableModels").mockResolvedValue([]);
render(<S3VectorsConfig {...defaultProps} />);
const regionInput = screen.getByPlaceholderText("us-west-2");
await act(async () => {
fireEvent.change(regionInput, { target: { value: "us-east-1" } });
});
expect(mockOnParamsChange).toHaveBeenCalledWith({
aws_region_name: "us-east-1",
});
});
it("should call onParamsChange when embedding model is selected", async () => {
const mockModels = [
{ model_group: "text-embedding-3-small", mode: "embedding" },
{ model_group: "text-embedding-3-large", mode: "embedding" },
];
vi.spyOn(fetchModels, "fetchAvailableModels").mockResolvedValue(mockModels);
render(<S3VectorsConfig {...defaultProps} />);
await waitFor(() => {
expect(fetchModels.fetchAvailableModels).toHaveBeenCalled();
});
// Find the Select component and trigger change directly
const selectElement = screen.getByRole("combobox");
await act(async () => {
// Simulate selecting a value by firing the change event
fireEvent.change(selectElement, { target: { value: "text-embedding-3-small" } });
});
// The component should handle the selection
expect(screen.getByText("Embedding Model")).toBeInTheDocument();
});
it("should preserve existing params when updating a field", async () => {
vi.spyOn(fetchModels, "fetchAvailableModels").mockResolvedValue([]);
const existingParams = {
vector_bucket_name: "existing-bucket",
aws_region_name: "us-west-2",
};
render(<S3VectorsConfig {...defaultProps} providerParams={existingParams} />);
const indexInput = screen.getByPlaceholderText("my-vector-index");
await act(async () => {
fireEvent.change(indexInput, { target: { value: "my-index" } });
});
expect(mockOnParamsChange).toHaveBeenCalledWith({
vector_bucket_name: "existing-bucket",
aws_region_name: "us-west-2",
index_name: "my-index",
});
});
it("should display existing param values", () => {
vi.spyOn(fetchModels, "fetchAvailableModels").mockResolvedValue([]);
const existingParams = {
vector_bucket_name: "my-bucket",
index_name: "my-index",
aws_region_name: "eu-west-1",
embedding_model: "text-embedding-3-small",
};
render(<S3VectorsConfig {...defaultProps} providerParams={existingParams} />);
expect(screen.getByDisplayValue("my-bucket")).toBeInTheDocument();
expect(screen.getByDisplayValue("my-index")).toBeInTheDocument();
expect(screen.getByDisplayValue("eu-west-1")).toBeInTheDocument();
});
it("should handle model fetch error gracefully", async () => {
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
vi.spyOn(fetchModels, "fetchAvailableModels").mockRejectedValue(new Error("Failed to fetch models"));
render(<S3VectorsConfig {...defaultProps} />);
await waitFor(() => {
expect(consoleErrorSpy).toHaveBeenCalledWith("Error fetching embedding models:", expect.any(Error));
});
consoleErrorSpy.mockRestore();
});
it("should not fetch models if accessToken is null", () => {
const fetchSpy = vi.spyOn(fetchModels, "fetchAvailableModels");
render(<S3VectorsConfig {...defaultProps} accessToken={null} />);
expect(fetchSpy).not.toHaveBeenCalled();
});
});

View file

@ -0,0 +1,172 @@
import React, { useState, useEffect } from "react";
import { Alert, Form, Input, Select, Tooltip } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import { fetchAvailableModels, ModelGroup } from "../playground/llm_calls/fetch_models";
interface S3VectorsConfigProps {
accessToken: string | null;
providerParams: Record<string, any>;
onParamsChange: (params: Record<string, any>) => void;
}
const S3VectorsConfig: React.FC<S3VectorsConfigProps> = ({
accessToken,
providerParams,
onParamsChange,
}) => {
const [embeddingModels, setEmbeddingModels] = useState<ModelGroup[]>([]);
const [isLoadingModels, setIsLoadingModels] = useState(false);
useEffect(() => {
if (!accessToken) return;
const loadModels = async () => {
setIsLoadingModels(true);
try {
const models = await fetchAvailableModels(accessToken);
// Filter for embedding models only
const embeddingOnly = models.filter((model) => model.mode === "embedding");
setEmbeddingModels(embeddingOnly);
} catch (error) {
console.error("Error fetching embedding models:", error);
} finally {
setIsLoadingModels(false);
}
};
loadModels();
}, [accessToken]);
const handleFieldChange = (fieldName: string, value: string) => {
onParamsChange({
...providerParams,
[fieldName]: value,
});
};
return (
<>
{/* S3 Vectors Setup Instructions */}
<Alert
message="AWS S3 Vectors Setup"
description={
<div>
<p>AWS S3 Vectors allows you to store and query vector embeddings directly in S3:</p>
<ul style={{ marginLeft: "16px", marginTop: "8px" }}>
<li>Vector buckets and indexes will be automatically created if they don't exist</li>
<li>Vector dimensions are auto-detected from your selected embedding model</li>
<li>Ensure your AWS credentials have permissions for S3 Vectors operations</li>
<li>
Learn more:{" "}
<a
href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-vector-buckets.html"
target="_blank"
rel="noopener noreferrer"
>
AWS S3 Vectors Documentation
</a>
</li>
</ul>
</div>
}
type="info"
showIcon
style={{ marginBottom: "16px" }}
/>
{/* Vector Bucket Name */}
<Form.Item
label={
<span>
Vector Bucket Name{" "}
<Tooltip title="S3 bucket name for vector storage (will be auto-created if it doesn't exist)">
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</Tooltip>
</span>
}
required
>
<Input
value={providerParams.vector_bucket_name || ""}
onChange={(e) => handleFieldChange("vector_bucket_name", e.target.value)}
placeholder="my-vector-bucket"
size="large"
className="rounded-md"
/>
</Form.Item>
{/* Index Name (Optional) */}
<Form.Item
label={
<span>
Index Name{" "}
<Tooltip title="Name for the vector index (optional, will be auto-generated if not provided)">
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</Tooltip>
</span>
}
>
<Input
value={providerParams.index_name || ""}
onChange={(e) => handleFieldChange("index_name", e.target.value)}
placeholder="my-vector-index"
size="large"
className="rounded-md"
/>
</Form.Item>
{/* AWS Region */}
<Form.Item
label={
<span>
AWS Region{" "}
<Tooltip title="AWS region where the S3 bucket is located (e.g., us-west-2)">
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</Tooltip>
</span>
}
required
>
<Input
value={providerParams.aws_region_name || ""}
onChange={(e) => handleFieldChange("aws_region_name", e.target.value)}
placeholder="us-west-2"
size="large"
className="rounded-md"
/>
</Form.Item>
{/* Embedding Model */}
<Form.Item
label={
<span>
Embedding Model{" "}
<Tooltip title="Select the embedding model to use for vector generation">
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</Tooltip>
</span>
}
required
>
<Select
value={providerParams.embedding_model || undefined}
onChange={(value) => handleFieldChange("embedding_model", value)}
placeholder="Select an embedding model"
size="large"
showSearch
loading={isLoadingModels}
filterOption={(input, option) =>
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
}
options={embeddingModels.map((model) => ({
value: model.model_group,
label: model.model_group,
}))}
style={{ width: "100%" }}
/>
</Form.Item>
</>
);
};
export default S3VectorsConfig;

View file

@ -5,6 +5,7 @@ export enum VectorStoreProviders {
OpenAI = "OpenAI",
Azure = "Azure OpenAI",
Milvus = "Milvus",
S3Vectors = "AWS S3 Vectors",
}
export const vectorStoreProviderMap: Record<string, string> = {
@ -14,6 +15,7 @@ export const vectorStoreProviderMap: Record<string, string> = {
OpenAI: "openai",
Azure: "azure",
Milvus: "milvus",
S3Vectors: "s3_vectors",
};
const asset_logos_folder = "../ui/assets/logos/";
@ -25,6 +27,7 @@ export const vectorStoreProviderLogoMap: Record<string, string> = {
[VectorStoreProviders.OpenAI]: `${asset_logos_folder}openai_small.svg`,
[VectorStoreProviders.Azure]: `${asset_logos_folder}microsoft_azure.svg`,
[VectorStoreProviders.Milvus]: `${asset_logos_folder}milvus.svg`,
[VectorStoreProviders.S3Vectors]: `${asset_logos_folder}aws.svg`,
};
// Define field types for provider-specific configurations
@ -114,6 +117,40 @@ export const vectorStoreProviderFields: Record<string, VectorStoreFieldConfig[]>
type: "select",
},
],
s3_vectors: [
{
name: "vector_bucket_name",
label: "Vector Bucket Name",
tooltip: "S3 bucket name for vector storage (will be auto-created if it doesn't exist)",
placeholder: "my-vector-bucket",
required: true,
type: "text",
},
{
name: "index_name",
label: "Index Name",
tooltip: "Name for the vector index (optional, will be auto-generated if not provided)",
placeholder: "my-vector-index",
required: false,
type: "text",
},
{
name: "aws_region_name",
label: "AWS Region",
tooltip: "AWS region where the S3 bucket is located (e.g., us-west-2)",
placeholder: "us-west-2",
required: true,
type: "text",
},
{
name: "embedding_model",
label: "Embedding Model",
tooltip: "Select the embedding model to use for vector generation",
placeholder: "text-embedding-3-small",
required: true,
type: "select",
},
],
};
export const getVectorStoreProviderLogoAndName = (providerValue: string): { logo: string; displayName: string } => {