refactor ui

This commit is contained in:
Ishaan Jaffer 2026-02-04 18:19:33 -08:00
parent e07ab1d89d
commit e25f7f1f7a
4 changed files with 461 additions and 110 deletions

View file

@ -7,8 +7,10 @@ import {
Alert,
Button,
Card,
Col,
Form,
InputNumber,
Row,
Select,
Skeleton,
Slider,
@ -20,6 +22,8 @@ import {
import { QuestionCircleOutlined, CheckCircleOutlined, SaveOutlined } from "@ant-design/icons";
import { useEffect, useState } from "react";
import { fetchAvailableModels, ModelGroup } from "@/components/playground/llm_calls/fetch_models";
import MCPSemanticFilterTestPanel from "./MCPSemanticFilterTestPanel";
import { getCurlCommand, runSemanticFilterTest, TestResult } from "./semanticFilterTestUtils";
interface MCPSemanticFilterSettingsProps {
accessToken: string | null;
@ -37,6 +41,13 @@ export default function MCPSemanticFilterSettings({ accessToken }: MCPSemanticFi
const [isDirty, setIsDirty] = useState(false);
const [embeddingModels, setEmbeddingModels] = useState<ModelGroup[]>([]);
const [loadingModels, setLoadingModels] = useState(true);
// Test section state
const [testQuery, setTestQuery] = useState("");
const [testModel, setTestModel] = useState<string>("gpt-4o");
const [testResult, setTestResult] = useState<TestResult | null>(null);
const [isTesting, setIsTesting] = useState(false);
const [showCurl, setShowCurl] = useState(false);
const schema = data?.field_schema;
const values = data?.values ?? {};
@ -92,6 +103,20 @@ export default function MCPSemanticFilterSettings({ accessToken }: MCPSemanticFi
}
};
const handleTest = async () => {
if (!accessToken) {
return;
}
await runSemanticFilterTest({
accessToken,
testModel,
testQuery,
setIsTesting,
setTestResult,
});
};
if (!accessToken) {
return (
<div className="p-6 text-center text-gray-500">
@ -101,7 +126,7 @@ export default function MCPSemanticFilterSettings({ accessToken }: MCPSemanticFi
}
return (
<div style={{ maxWidth: 800 }}>
<div style={{ width: "100%" }}>
{isLoading ? (
<Skeleton active />
) : isError ? (
@ -143,120 +168,143 @@ export default function MCPSemanticFilterSettings({ accessToken }: MCPSemanticFi
/>
)}
<Form
form={form}
layout="vertical"
disabled={isUpdating}
onValuesChange={() => {
setIsDirty(true);
}}
>
<Card style={{ marginBottom: 16 }}>
<Form.Item
name="enabled"
label={
<Space>
<Typography.Text strong>Enable Semantic Filtering</Typography.Text>
<Tooltip title="When enabled, only the most relevant MCP tools will be included in requests based on semantic similarity">
<QuestionCircleOutlined style={{ color: "#8c8c8c" }} />
</Tooltip>
</Space>
}
valuePropName="checked"
<Row gutter={24}>
{/* Left Column - Settings */}
<Col xs={24} lg={12}>
<Form
form={form}
layout="vertical"
disabled={isUpdating}
onValuesChange={() => {
setIsDirty(true);
}}
>
<Switch disabled={isUpdating} />
</Form.Item>
<Typography.Text type="secondary" style={{ display: "block", marginTop: -16, marginBottom: 16 }}>
{schema?.properties?.enabled?.description}
</Typography.Text>
</Card>
<Card style={{ marginBottom: 16 }}>
<Form.Item
name="enabled"
label={
<Space>
<Typography.Text strong>Enable Semantic Filtering</Typography.Text>
<Tooltip title="When enabled, only the most relevant MCP tools will be included in requests based on semantic similarity">
<QuestionCircleOutlined style={{ color: "#8c8c8c" }} />
</Tooltip>
</Space>
}
valuePropName="checked"
>
<Switch disabled={isUpdating} />
</Form.Item>
<Typography.Text type="secondary" style={{ display: "block", marginTop: -16, marginBottom: 16 }}>
{schema?.properties?.enabled?.description}
</Typography.Text>
</Card>
<Card title="Configuration" style={{ marginBottom: 16 }}>
<Form.Item
name="embedding_model"
label={
<Space>
<Typography.Text strong>Embedding Model</Typography.Text>
<Tooltip title="The model used to generate embeddings for semantic matching">
<QuestionCircleOutlined style={{ color: "#8c8c8c" }} />
</Tooltip>
</Space>
}
>
<Select
options={embeddingModels.map((model) => ({
label: model.model_group,
value: model.model_group,
}))}
placeholder={loadingModels ? "Loading models..." : "Select embedding model"}
showSearch
disabled={isUpdating || loadingModels}
loading={loadingModels}
notFoundContent={
loadingModels ? "Loading..." : "No embedding models available"
}
/>
</Form.Item>
<Card title="Configuration" style={{ marginBottom: 16 }}>
<Form.Item
name="embedding_model"
label={
<Space>
<Typography.Text strong>Embedding Model</Typography.Text>
<Tooltip title="The model used to generate embeddings for semantic matching">
<QuestionCircleOutlined style={{ color: "#8c8c8c" }} />
</Tooltip>
</Space>
}
>
<Select
options={embeddingModels.map((model) => ({
label: model.model_group,
value: model.model_group,
}))}
placeholder={loadingModels ? "Loading models..." : "Select embedding model"}
showSearch
disabled={isUpdating || loadingModels}
loading={loadingModels}
notFoundContent={
loadingModels ? "Loading..." : "No embedding models available"
}
/>
</Form.Item>
<Form.Item
name="top_k"
label={
<Space>
<Typography.Text strong>Top K Results</Typography.Text>
<Tooltip title="Maximum number of tools to return after filtering">
<QuestionCircleOutlined style={{ color: "#8c8c8c" }} />
</Tooltip>
</Space>
}
>
<InputNumber
min={1}
max={100}
style={{ width: "100%" }}
disabled={isUpdating}
/>
</Form.Item>
<Form.Item
name="top_k"
label={
<Space>
<Typography.Text strong>Top K Results</Typography.Text>
<Tooltip title="Maximum number of tools to return after filtering">
<QuestionCircleOutlined style={{ color: "#8c8c8c" }} />
</Tooltip>
</Space>
}
>
<InputNumber
min={1}
max={100}
style={{ width: "100%" }}
disabled={isUpdating}
/>
</Form.Item>
<Form.Item
name="similarity_threshold"
label={
<Space>
<Typography.Text strong>Similarity Threshold</Typography.Text>
<Tooltip title="Minimum similarity score (0-1) for a tool to be included">
<QuestionCircleOutlined style={{ color: "#8c8c8c" }} />
</Tooltip>
</Space>
}
>
<Slider
min={0}
max={1}
step={0.05}
marks={{
0: "0.0",
0.3: "0.3",
0.5: "0.5",
0.7: "0.7",
1: "1.0",
}}
disabled={isUpdating}
/>
</Form.Item>
</Card>
<Form.Item
name="similarity_threshold"
label={
<Space>
<Typography.Text strong>Similarity Threshold</Typography.Text>
<Tooltip title="Minimum similarity score (0-1) for a tool to be included">
<QuestionCircleOutlined style={{ color: "#8c8c8c" }} />
</Tooltip>
</Space>
}
>
<Slider
min={0}
max={1}
step={0.05}
marks={{
0: "0.0",
0.3: "0.3",
0.5: "0.5",
0.7: "0.7",
1: "1.0",
}}
disabled={isUpdating}
/>
</Form.Item>
</Card>
<div style={{ display: "flex", justifyContent: "flex-end", gap: 8 }}>
<Button
type="primary"
icon={<SaveOutlined />}
onClick={handleSave}
loading={isUpdating}
disabled={!isDirty}
>
Save Settings
</Button>
</div>
</Form>
<div style={{ display: "flex", justifyContent: "flex-end", gap: 8 }}>
<Button
type="primary"
icon={<SaveOutlined />}
onClick={handleSave}
loading={isUpdating}
disabled={!isDirty}
>
Save Settings
</Button>
</div>
</Form>
</Col>
{/* Right Column - Test Configuration */}
<Col xs={24} lg={12}>
<MCPSemanticFilterTestPanel
accessToken={accessToken}
testQuery={testQuery}
setTestQuery={setTestQuery}
testModel={testModel}
setTestModel={setTestModel}
isTesting={isTesting}
onTest={handleTest}
filterEnabled={!!values.enabled}
testResult={testResult}
showCurl={showCurl}
setShowCurl={setShowCurl}
curlCommand={getCurlCommand(testModel, testQuery)}
/>
</Col>
</Row>
</>
)}
</div>

View file

@ -0,0 +1,144 @@
import { CodeOutlined, PlayCircleOutlined } from "@ant-design/icons";
import { Alert, Button, Card, Input, Space, Typography } from "antd";
import ModelSelector from "@/components/common_components/ModelSelector";
import { TestResult } from "./semanticFilterTestUtils";
interface MCPSemanticFilterTestPanelProps {
accessToken: string | null;
testQuery: string;
setTestQuery: (value: string) => void;
testModel: string;
setTestModel: (value: string) => void;
isTesting: boolean;
onTest: () => void;
filterEnabled: boolean;
testResult: TestResult | null;
showCurl: boolean;
setShowCurl: (value: boolean) => void;
curlCommand: string;
}
export default function MCPSemanticFilterTestPanel({
accessToken,
testQuery,
setTestQuery,
testModel,
setTestModel,
isTesting,
onTest,
filterEnabled,
testResult,
showCurl,
setShowCurl,
curlCommand,
}: MCPSemanticFilterTestPanelProps) {
return (
<>
<Card title="Test Configuration" style={{ marginBottom: 16 }}>
<Space direction="vertical" style={{ width: "100%" }} size="large">
<div>
<Typography.Text strong style={{ display: "block", marginBottom: 8 }}>
<PlayCircleOutlined /> Test Query
</Typography.Text>
<Input.TextArea
placeholder="Enter a test query to see which tools would be selected..."
value={testQuery}
onChange={(e) => setTestQuery(e.target.value)}
rows={4}
disabled={isTesting}
/>
</div>
<div>
<ModelSelector
accessToken={accessToken || ""}
value={testModel}
onChange={setTestModel}
disabled={isTesting}
showLabel={true}
labelText="Select Model"
/>
</div>
<Button
type="primary"
icon={<PlayCircleOutlined />}
onClick={onTest}
loading={isTesting}
disabled={!testQuery || !testModel || !filterEnabled}
block
>
Test Filter
</Button>
{!filterEnabled && (
<Alert
type="warning"
message="Semantic filtering is disabled"
description="Enable semantic filtering and save settings to test the filter."
showIcon
/>
)}
{testResult && (
<div>
<Typography.Title level={5}>Results</Typography.Title>
<Alert
type="success"
message={`${testResult.selectedTools} tools selected`}
description={`Filtered from ${testResult.totalTools} available tools`}
showIcon
style={{ marginBottom: 16 }}
/>
<div>
<Typography.Text strong style={{ display: "block", marginBottom: 8 }}>
Selected Tools:
</Typography.Text>
<ul style={{ paddingLeft: 20, margin: 0 }}>
{testResult.tools.map((tool, index) => (
<li key={index} style={{ marginBottom: 4 }}>
<Typography.Text>{tool}</Typography.Text>
</li>
))}
</ul>
</div>
</div>
)}
</Space>
</Card>
<Card
title={
<Space>
<CodeOutlined />
<Typography.Text>API Usage</Typography.Text>
</Space>
}
>
<Typography.Text type="secondary" style={{ display: "block", marginBottom: 12 }}>
Use this curl command to test the semantic filter with your current configuration.
</Typography.Text>
<Button
type="link"
onClick={() => setShowCurl(!showCurl)}
style={{ padding: 0, marginBottom: 12 }}
>
{showCurl ? "Hide" : "Show"} curl command
</Button>
{showCurl && (
<pre
style={{
background: "#f5f5f5",
padding: 12,
borderRadius: 4,
overflow: "auto",
fontSize: 12,
}}
>
{curlCommand}
</pre>
)}
</Card>
</>
);
}

View file

@ -0,0 +1,95 @@
import NotificationManager from "@/components/molecules/notifications_manager";
import { testMCPSemanticFilter } from "@/components/networking";
export interface TestResult {
totalTools: number;
selectedTools: number;
tools: string[];
}
interface FilterHeaders {
filter: string | null;
tools: string | null;
}
const parseFilterHeaders = (headers: FilterHeaders): TestResult | null => {
if (!headers.filter) {
return null;
}
const [total, selected] = headers.filter.split("->").map(Number);
const tools = headers.tools
? headers.tools.split(",").map((name) => name.trim())
: [];
return { totalTools: total, selectedTools: selected, tools };
};
export const runSemanticFilterTest = async ({
accessToken,
testModel,
testQuery,
setIsTesting,
setTestResult,
}: {
accessToken: string;
testModel: string;
testQuery: string;
setIsTesting: (value: boolean) => void;
setTestResult: (result: TestResult | null) => void;
}) => {
if (!testQuery || !testModel || !accessToken) {
NotificationManager.error("Please enter a query and select a model");
return;
}
setIsTesting(true);
setTestResult(null);
try {
const { headers } = await testMCPSemanticFilter(
accessToken,
testModel,
testQuery
);
const parsedResult = parseFilterHeaders(headers);
if (!parsedResult) {
NotificationManager.warning(
"Semantic filter is not enabled or no tools were filtered"
);
return;
}
setTestResult(parsedResult);
NotificationManager.success("Semantic filter test completed successfully");
} catch (error) {
console.error("Test failed:", error);
NotificationManager.error("Failed to test semantic filter");
} finally {
setIsTesting(false);
}
};
export const getCurlCommand = (testModel: string, testQuery: string) =>
`curl --location 'http://localhost:4000/v1/responses' \\
--header 'Content-Type: application/json' \\
--header 'Authorization: Bearer sk-1234' \\
--data '{
"model": "${testModel}",
"input": [
{
"role": "user",
"content": "${testQuery || "Your query here"}",
"type": "message"
}
],
"tools": [
{
"type": "mcp",
"server_url": "litellm_proxy",
"require_approval": "never"
}
],
"tool_choice": "required"
}'`;

View file

@ -5465,6 +5465,70 @@ export const updateMCPSemanticFilterSettings = async (
}
};
export const testMCPSemanticFilter = async (
accessToken: string,
model: string,
query: string
) => {
/**
* Test MCP semantic filter by making a responses API call
* Returns both the response data and headers containing filter information
*/
try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/v1/responses` : `/v1/responses`;
const response = await fetch(url, {
method: "POST",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: model,
input: [
{
role: "user",
content: query,
type: "message",
},
],
tools: [
{
type: "mcp",
server_url: "litellm_proxy",
require_approval: "never",
},
],
tool_choice: "required",
}),
});
// Extract headers before checking response status
const filterHeader = response.headers.get("x-litellm-semantic-filter");
const toolsHeader = response.headers.get("x-litellm-semantic-filter-tools");
if (!response.ok) {
const errorData = await response.json();
const errorMessage = deriveErrorMessage(errorData);
handleError(errorMessage);
throw new Error(errorMessage);
}
const data = await response.json();
// Return both data and headers
return {
data,
headers: {
filter: filterHeader,
tools: toolsHeader,
},
};
} catch (error) {
console.error("Failed to test MCP semantic filter:", error);
throw error;
}
};
export const getGuardrailsList = async (accessToken: string) => {
try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/v2/guardrails/list` : `/v2/guardrails/list`;