mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
refactor ui
This commit is contained in:
parent
e07ab1d89d
commit
e25f7f1f7a
4 changed files with 461 additions and 110 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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"
|
||||
}'`;
|
||||
|
|
@ -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`;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue