mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
[Feat] UI - Agent Gateway - set allowed agents by key, team (#17511)
* init schema.prisma * init LiteLLM_ObjectPermissionTable with agents and agent_access_groups * TestAgentRequestHandler * refatctor agent list * add AgentRequestHandler * fix agent access controls by key/team * feat - new migration for LiteLLM_AgentsTable * fix add LiteLLM_ObjectPermissionBase with agent and agent groups * add agent routes to llm api routes * add agent routes as llm route * add AgentPermissionsProps * add agents on team/key create * add agent selector on team/key * add agent selector on key edit /info * add AgentPermissions * docs list + invoke agents
This commit is contained in:
parent
8f592088ed
commit
575e769bff
11 changed files with 492 additions and 14 deletions
|
|
@ -33,10 +33,12 @@ The URL should be the invocation URL for your A2A agent (e.g., `http://localhost
|
|||
|
||||
## Invoking your Agents
|
||||
|
||||
Use the [A2A Python SDK](https://pypi.org/project/a2a/) to invoke agents through LiteLLM:
|
||||
Use the [A2A Python SDK](https://pypi.org/project/a2a/) to invoke agents through LiteLLM.
|
||||
|
||||
- `base_url`: Your LiteLLM proxy URL + `/a2a/{agent_name}`
|
||||
- `headers`: Include your LiteLLM Virtual Key for authentication
|
||||
This example shows how to:
|
||||
1. **List available agents** - Query `/v1/agents` to see which agents your key can access
|
||||
2. **Select an agent** - Pick an agent from the list
|
||||
3. **Invoke via A2A** - Use the A2A protocol to send messages to the agent
|
||||
|
||||
```python showLineNumbers title="invoke_a2a_agent.py"
|
||||
from uuid import uuid4
|
||||
|
|
@ -48,20 +50,36 @@ from a2a.types import MessageSendParams, SendMessageRequest
|
|||
# === CONFIGURE THESE ===
|
||||
LITELLM_BASE_URL = "http://localhost:4000" # Your LiteLLM proxy URL
|
||||
LITELLM_VIRTUAL_KEY = "sk-1234" # Your LiteLLM Virtual Key
|
||||
LITELLM_AGENT_NAME = "ij-local" # Agent name registered in LiteLLM
|
||||
# =======================
|
||||
|
||||
async def main():
|
||||
base_url = f"{LITELLM_BASE_URL}/a2a/{LITELLM_AGENT_NAME}"
|
||||
headers = {"Authorization": f"Bearer {LITELLM_VIRTUAL_KEY}"}
|
||||
|
||||
async with httpx.AsyncClient(headers=headers) as httpx_client:
|
||||
# Resolve agent card and create client
|
||||
resolver = A2ACardResolver(httpx_client=httpx_client, base_url=base_url)
|
||||
async with httpx.AsyncClient(headers=headers) as client:
|
||||
# Step 1: List available agents
|
||||
response = await client.get(f"{LITELLM_BASE_URL}/v1/agents")
|
||||
agents = response.json()
|
||||
|
||||
print("Available agents:")
|
||||
for agent in agents:
|
||||
print(f" - {agent['agent_name']} (ID: {agent['agent_id']})")
|
||||
|
||||
if not agents:
|
||||
print("No agents available for this key")
|
||||
return
|
||||
|
||||
# Step 2: Select an agent and invoke it
|
||||
selected_agent = agents[0]
|
||||
agent_id = selected_agent["agent_id"]
|
||||
agent_name = selected_agent["agent_name"]
|
||||
print(f"\nInvoking: {agent_name}")
|
||||
|
||||
# Step 3: Use A2A protocol to invoke the agent
|
||||
base_url = f"{LITELLM_BASE_URL}/a2a/{agent_id}"
|
||||
resolver = A2ACardResolver(httpx_client=client, base_url=base_url)
|
||||
agent_card = await resolver.get_agent_card()
|
||||
client = A2AClient(httpx_client=httpx_client, agent_card=agent_card)
|
||||
|
||||
# Send a message
|
||||
a2a_client = A2AClient(httpx_client=client, agent_card=agent_card)
|
||||
|
||||
request = SendMessageRequest(
|
||||
id=str(uuid4()),
|
||||
params=MessageSendParams(
|
||||
|
|
@ -72,8 +90,8 @@ async def main():
|
|||
}
|
||||
),
|
||||
)
|
||||
response = await client.send_message(request)
|
||||
print(response.model_dump(mode="json", exclude_none=True))
|
||||
response = await a2a_client.send_message(request)
|
||||
print(f"Response: {response.model_dump(mode='json', exclude_none=True, indent=4)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
import NumericalInput from "@/components/shared/numerical_input";
|
||||
import VectorStoreSelector from "@/components/vector_store_management/VectorStoreSelector";
|
||||
import MCPServerSelector from "@/components/mcp_server_management/MCPServerSelector";
|
||||
import AgentSelector from "@/components/agent_management/AgentSelector";
|
||||
import PremiumLoggingSettings from "@/components/common_components/PremiumLoggingSettings";
|
||||
import ModelAliasManager from "@/components/common_components/ModelAliasManager";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
|
@ -210,6 +211,21 @@ const CreateTeamModal = ({
|
|||
formValues.object_permission.mcp_tool_permissions = formValues.mcp_tool_permissions;
|
||||
delete formValues.mcp_tool_permissions;
|
||||
}
|
||||
|
||||
// Handle agent permissions
|
||||
if (formValues.allowed_agents_and_groups) {
|
||||
const { agents, accessGroups } = formValues.allowed_agents_and_groups;
|
||||
if (!formValues.object_permission) {
|
||||
formValues.object_permission = {};
|
||||
}
|
||||
if (agents && agents.length > 0) {
|
||||
formValues.object_permission.agents = agents;
|
||||
}
|
||||
if (accessGroups && accessGroups.length > 0) {
|
||||
formValues.object_permission.agent_access_groups = accessGroups;
|
||||
}
|
||||
delete formValues.allowed_agents_and_groups;
|
||||
}
|
||||
}
|
||||
|
||||
// Transform allowed_mcp_access_groups into object_permission
|
||||
|
|
@ -546,6 +562,34 @@ const CreateTeamModal = ({
|
|||
</AccordionBody>
|
||||
</Accordion>
|
||||
|
||||
<Accordion className="mt-8 mb-8">
|
||||
<AccordionHeader>
|
||||
<b>Agent Settings</b>
|
||||
</AccordionHeader>
|
||||
<AccordionBody>
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Allowed Agents{" "}
|
||||
<Tooltip title="Select which agents or access groups this team can access">
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="allowed_agents_and_groups"
|
||||
className="mt-4"
|
||||
help="Select agents or access groups this team can access"
|
||||
>
|
||||
<AgentSelector
|
||||
onChange={(val: any) => form.setFieldValue("allowed_agents_and_groups", val)}
|
||||
value={form.getFieldValue("allowed_agents_and_groups")}
|
||||
accessToken={accessToken || ""}
|
||||
placeholder="Select agents or access groups (optional)"
|
||||
/>
|
||||
</Form.Item>
|
||||
</AccordionBody>
|
||||
</Accordion>
|
||||
|
||||
<Accordion className="mt-8 mb-8">
|
||||
<AccordionHeader>
|
||||
<b>Logging Settings</b>
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ import {
|
|||
import type { KeyResponse, Team } from "./key_team_helpers/key_list";
|
||||
import MCPServerSelector from "./mcp_server_management/MCPServerSelector";
|
||||
import MCPToolPermissions from "./mcp_server_management/MCPToolPermissions";
|
||||
import AgentSelector from "./agent_management/AgentSelector";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
import { Organization, fetchMCPAccessGroups, getGuardrailsList, teamDeleteCall } from "./networking";
|
||||
import NumericalInput from "./shared/numerical_input";
|
||||
|
|
@ -448,6 +449,21 @@ const Teams: React.FC<TeamProps> = ({
|
|||
delete formValues.allowed_mcp_access_groups;
|
||||
}
|
||||
|
||||
// Handle agent permissions
|
||||
if (formValues.allowed_agents_and_groups) {
|
||||
const { agents, accessGroups } = formValues.allowed_agents_and_groups;
|
||||
if (!formValues.object_permission) {
|
||||
formValues.object_permission = {};
|
||||
}
|
||||
if (agents && agents.length > 0) {
|
||||
formValues.object_permission.agents = agents;
|
||||
}
|
||||
if (accessGroups && accessGroups.length > 0) {
|
||||
formValues.object_permission.agent_access_groups = accessGroups;
|
||||
}
|
||||
delete formValues.allowed_agents_and_groups;
|
||||
}
|
||||
|
||||
// Add model_aliases if any are defined
|
||||
if (Object.keys(modelAliases).length > 0) {
|
||||
formValues.model_aliases = modelAliases;
|
||||
|
|
@ -1370,6 +1386,34 @@ const Teams: React.FC<TeamProps> = ({
|
|||
</AccordionBody>
|
||||
</Accordion>
|
||||
|
||||
<Accordion className="mt-8 mb-8">
|
||||
<AccordionHeader>
|
||||
<b>Agent Settings</b>
|
||||
</AccordionHeader>
|
||||
<AccordionBody>
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Allowed Agents{" "}
|
||||
<Tooltip title="Select which agents or access groups this team can access">
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="allowed_agents_and_groups"
|
||||
className="mt-4"
|
||||
help="Select agents or access groups this team can access"
|
||||
>
|
||||
<AgentSelector
|
||||
onChange={(val: any) => form.setFieldValue("allowed_agents_and_groups", val)}
|
||||
value={form.getFieldValue("allowed_agents_and_groups")}
|
||||
accessToken={accessToken || ""}
|
||||
placeholder="Select agents or access groups (optional)"
|
||||
/>
|
||||
</Form.Item>
|
||||
</AccordionBody>
|
||||
</Accordion>
|
||||
|
||||
<Accordion className="mt-8 mb-8">
|
||||
<AccordionHeader>
|
||||
<b>Logging Settings</b>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,147 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { Select } from "antd";
|
||||
import { getAgentsList } from "../networking";
|
||||
|
||||
interface Agent {
|
||||
agent_id: string;
|
||||
agent_name: string;
|
||||
agent_config?: Record<string, any>;
|
||||
agent_card_params?: Record<string, any>;
|
||||
}
|
||||
|
||||
interface AgentSelectorProps {
|
||||
onChange: (selected: {
|
||||
agents: string[];
|
||||
accessGroups: string[];
|
||||
}) => void;
|
||||
value?: {
|
||||
agents: string[];
|
||||
accessGroups: string[];
|
||||
};
|
||||
className?: string;
|
||||
accessToken: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const AgentSelector: React.FC<AgentSelectorProps> = ({
|
||||
onChange,
|
||||
value,
|
||||
className,
|
||||
accessToken,
|
||||
placeholder = "Select agents",
|
||||
disabled = false,
|
||||
}) => {
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
const [accessGroups, setAccessGroups] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
if (!accessToken) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await getAgentsList(accessToken);
|
||||
let agentsList = response?.agents || [];
|
||||
setAgents(agentsList);
|
||||
|
||||
// Extract unique access groups from agents
|
||||
const groups = new Set<string>();
|
||||
agentsList.forEach((agent: Agent) => {
|
||||
const agentAccessGroups = (agent as any).agent_access_groups;
|
||||
if (agentAccessGroups && Array.isArray(agentAccessGroups)) {
|
||||
agentAccessGroups.forEach((g: string) => groups.add(g));
|
||||
}
|
||||
});
|
||||
setAccessGroups(Array.from(groups));
|
||||
} catch (error) {
|
||||
console.error("Error fetching agents:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, [accessToken]);
|
||||
|
||||
// Combine options, access groups first
|
||||
const options = [
|
||||
...accessGroups.map((group) => ({
|
||||
label: group,
|
||||
value: `group:${group}`,
|
||||
isAccessGroup: true,
|
||||
searchText: `${group} Access Group`,
|
||||
})),
|
||||
...agents.map((agent) => ({
|
||||
label: `${agent.agent_name || agent.agent_id}`,
|
||||
value: agent.agent_id,
|
||||
isAccessGroup: false,
|
||||
searchText: `${agent.agent_name || agent.agent_id} ${agent.agent_id} Agent`,
|
||||
})),
|
||||
];
|
||||
|
||||
// Flatten value for Select
|
||||
const selectedValues = [
|
||||
...(value?.agents || []),
|
||||
...(value?.accessGroups || []).map((g) => `group:${g}`),
|
||||
];
|
||||
|
||||
// Handle selection
|
||||
const handleChange = (selected: string[]) => {
|
||||
const agentsSelected = selected.filter((v) => !v.startsWith("group:"));
|
||||
const accessGroupsSelected = selected
|
||||
.filter((v) => v.startsWith("group:"))
|
||||
.map((v) => v.replace("group:", ""));
|
||||
onChange({ agents: agentsSelected, accessGroups: accessGroupsSelected });
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder={placeholder}
|
||||
onChange={handleChange}
|
||||
value={selectedValues}
|
||||
loading={loading}
|
||||
className={className}
|
||||
showSearch
|
||||
style={{ width: "100%" }}
|
||||
disabled={disabled}
|
||||
filterOption={(input, option) => {
|
||||
const searchText = options.find((opt) => opt.value === option?.value)?.searchText || "";
|
||||
return searchText.toLowerCase().includes(input.toLowerCase());
|
||||
}}
|
||||
>
|
||||
{options.map((opt) => (
|
||||
<Select.Option key={opt.value} value={opt.value} label={opt.label}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<span
|
||||
style={{
|
||||
display: "inline-block",
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: "50%",
|
||||
background: opt.isAccessGroup ? "#52c41a" : "#722ed1",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
<span style={{ flex: 1 }}>{opt.label}</span>
|
||||
<span
|
||||
style={{
|
||||
color: opt.isAccessGroup ? "#52c41a" : "#722ed1",
|
||||
fontSize: "12px",
|
||||
fontWeight: 500,
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
{opt.isAccessGroup ? "Access Group" : "Agent"}
|
||||
</span>
|
||||
</div>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentSelector;
|
||||
|
||||
|
|
@ -83,6 +83,8 @@ export interface KeyResponse {
|
|||
mcp_access_groups?: string[];
|
||||
mcp_tool_permissions?: Record<string, string[]>;
|
||||
vector_stores: string[];
|
||||
agents?: string[];
|
||||
agent_access_groups?: string[];
|
||||
};
|
||||
auto_rotate?: boolean;
|
||||
rotation_interval?: string;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React from "react";
|
|||
import { Text } from "@tremor/react";
|
||||
import VectorStorePermissions from "./permissions/VectorStorePermissions";
|
||||
import MCPServerPermissions from "./permissions/MCPServerPermissions";
|
||||
import AgentPermissions from "./permissions/AgentPermissions";
|
||||
|
||||
interface ObjectPermission {
|
||||
object_permission_id: string;
|
||||
|
|
@ -9,6 +10,8 @@ interface ObjectPermission {
|
|||
mcp_access_groups?: string[];
|
||||
mcp_tool_permissions?: Record<string, string[]>;
|
||||
vector_stores: string[];
|
||||
agents?: string[];
|
||||
agent_access_groups?: string[];
|
||||
}
|
||||
|
||||
interface ObjectPermissionsViewProps {
|
||||
|
|
@ -28,9 +31,11 @@ export function ObjectPermissionsView({
|
|||
const mcpServers = objectPermission?.mcp_servers || [];
|
||||
const mcpAccessGroups = objectPermission?.mcp_access_groups || [];
|
||||
const mcpToolPermissions = objectPermission?.mcp_tool_permissions || {};
|
||||
const agents = objectPermission?.agents || [];
|
||||
const agentAccessGroups = objectPermission?.agent_access_groups || [];
|
||||
|
||||
const content = (
|
||||
<div className={variant === "card" ? "grid grid-cols-1 md:grid-cols-2 gap-6" : "space-y-4"}>
|
||||
<div className={variant === "card" ? "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" : "space-y-4"}>
|
||||
<VectorStorePermissions vectorStores={vectorStores} accessToken={accessToken} />
|
||||
<MCPServerPermissions
|
||||
mcpServers={mcpServers}
|
||||
|
|
@ -38,6 +43,11 @@ export function ObjectPermissionsView({
|
|||
mcpToolPermissions={mcpToolPermissions}
|
||||
accessToken={accessToken}
|
||||
/>
|
||||
<AgentPermissions
|
||||
agents={agents}
|
||||
agentAccessGroups={agentAccessGroups}
|
||||
accessToken={accessToken}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import { formatNumberWithCommas } from "@/utils/dataUtils";
|
|||
import { mapDisplayToInternalNames } from "../callback_info_helpers";
|
||||
import MCPServerSelector from "../mcp_server_management/MCPServerSelector";
|
||||
import MCPToolPermissions from "../mcp_server_management/MCPToolPermissions";
|
||||
import AgentSelector from "../agent_management/AgentSelector";
|
||||
import ModelAliasManager from "../common_components/ModelAliasManager";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
import KeyLifecycleSettings from "../common_components/KeyLifecycleSettings";
|
||||
|
|
@ -385,6 +386,26 @@ const CreateKey: React.FC<CreateKeyProps> = ({
|
|||
delete formValues.allowed_mcp_access_groups;
|
||||
}
|
||||
|
||||
// Transform allowed_agents_and_groups into object_permission format
|
||||
if (
|
||||
formValues.allowed_agents_and_groups &&
|
||||
(formValues.allowed_agents_and_groups.agents?.length > 0 ||
|
||||
formValues.allowed_agents_and_groups.accessGroups?.length > 0)
|
||||
) {
|
||||
if (!formValues.object_permission) {
|
||||
formValues.object_permission = {};
|
||||
}
|
||||
const { agents, accessGroups } = formValues.allowed_agents_and_groups;
|
||||
if (agents && agents.length > 0) {
|
||||
formValues.object_permission.agents = agents;
|
||||
}
|
||||
if (accessGroups && accessGroups.length > 0) {
|
||||
formValues.object_permission.agent_access_groups = accessGroups;
|
||||
}
|
||||
// Remove the original field as it's now part of object_permission
|
||||
delete formValues.allowed_agents_and_groups;
|
||||
}
|
||||
|
||||
// Add model_aliases if any are defined
|
||||
if (Object.keys(modelAliases).length > 0) {
|
||||
formValues.aliases = JSON.stringify(modelAliases);
|
||||
|
|
@ -1091,6 +1112,33 @@ const CreateKey: React.FC<CreateKeyProps> = ({
|
|||
</AccordionBody>
|
||||
</Accordion>
|
||||
|
||||
<Accordion className="mt-4 mb-4">
|
||||
<AccordionHeader>
|
||||
<b>Agent Settings</b>
|
||||
</AccordionHeader>
|
||||
<AccordionBody>
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Allowed Agents{" "}
|
||||
<Tooltip title="Select which agents or access groups this key can access">
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="allowed_agents_and_groups"
|
||||
help="Select agents or access groups this key can access"
|
||||
>
|
||||
<AgentSelector
|
||||
onChange={(val: any) => form.setFieldValue("allowed_agents_and_groups", val)}
|
||||
value={form.getFieldValue("allowed_agents_and_groups")}
|
||||
accessToken={accessToken}
|
||||
placeholder="Select agents or access groups (optional)"
|
||||
/>
|
||||
</Form.Item>
|
||||
</AccordionBody>
|
||||
</Accordion>
|
||||
|
||||
{premiumUser ? (
|
||||
<Accordion className="mt-4 mb-4">
|
||||
<AccordionHeader>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,111 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Text, Badge } from "@tremor/react";
|
||||
import { UserGroupIcon } from "@heroicons/react/outline";
|
||||
import { Tooltip } from "antd";
|
||||
import { getAgentsList } from "../networking";
|
||||
|
||||
interface Agent {
|
||||
agent_id: string;
|
||||
agent_name: string;
|
||||
agent_config?: Record<string, any>;
|
||||
agent_card_params?: Record<string, any>;
|
||||
}
|
||||
|
||||
interface AgentPermissionsProps {
|
||||
agents: string[];
|
||||
agentAccessGroups?: string[];
|
||||
accessToken?: string | null;
|
||||
}
|
||||
|
||||
export function AgentPermissions({
|
||||
agents,
|
||||
agentAccessGroups = [],
|
||||
accessToken
|
||||
}: AgentPermissionsProps) {
|
||||
const [agentDetails, setAgentDetails] = useState<Agent[]>([]);
|
||||
|
||||
// Fetch agent details when component mounts
|
||||
useEffect(() => {
|
||||
const fetchAgentDetails = async () => {
|
||||
if (accessToken && agents.length > 0) {
|
||||
try {
|
||||
const response = await getAgentsList(accessToken);
|
||||
if (response && response.agents && Array.isArray(response.agents)) {
|
||||
setAgentDetails(response.agents);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching agents:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
fetchAgentDetails();
|
||||
}, [accessToken, agents.length]);
|
||||
|
||||
// Function to get display name for agent
|
||||
const getAgentDisplayName = (agentId: string) => {
|
||||
const agentDetail = agentDetails.find((agent) => agent.agent_id === agentId);
|
||||
if (agentDetail) {
|
||||
const truncatedId = agentId.length > 7 ? `${agentId.slice(0, 3)}...${agentId.slice(-4)}` : agentId;
|
||||
return `${agentDetail.agent_name} (${truncatedId})`;
|
||||
}
|
||||
return agentId;
|
||||
};
|
||||
|
||||
// Merge agents and access groups into one list
|
||||
const mergedItems = [
|
||||
...agents.map((agent) => ({ type: "agent", value: agent })),
|
||||
...agentAccessGroups.map((group) => ({ type: "accessGroup", value: group })),
|
||||
];
|
||||
const totalCount = mergedItems.length;
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<UserGroupIcon className="h-4 w-4 text-purple-600" />
|
||||
<Text className="font-semibold text-gray-900">Agents</Text>
|
||||
<Badge color="purple" size="xs">
|
||||
{totalCount}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{totalCount > 0 ? (
|
||||
<div className="max-h-[400px] overflow-y-auto space-y-2 pr-1">
|
||||
{mergedItems.map((item, index) => (
|
||||
<div key={index} className="space-y-2">
|
||||
<div
|
||||
className="flex items-center gap-3 py-2 px-3 rounded-lg border border-gray-200 bg-white"
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
{item.type === "agent" ? (
|
||||
<Tooltip title={`Full ID: ${item.value}`} placement="top">
|
||||
<div className="inline-flex items-center gap-2 min-w-0">
|
||||
<span className="inline-block w-1.5 h-1.5 bg-purple-500 rounded-full flex-shrink-0"></span>
|
||||
<span className="text-sm font-medium text-gray-900 truncate">{getAgentDisplayName(item.value)}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div className="inline-flex items-center gap-2 min-w-0">
|
||||
<span className="inline-block w-1.5 h-1.5 bg-green-500 rounded-full flex-shrink-0"></span>
|
||||
<span className="text-sm font-medium text-gray-900 truncate">{item.value}</span>
|
||||
<span className="ml-1 px-1.5 py-0.5 text-[9px] font-semibold text-green-600 bg-green-50 border border-green-200 rounded uppercase tracking-wide flex-shrink-0">
|
||||
Group
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-gray-50 border border-gray-200">
|
||||
<UserGroupIcon className="h-4 w-4 text-gray-400" />
|
||||
<Text className="text-gray-500 text-sm">No agents or access groups configured</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AgentPermissions;
|
||||
|
||||
|
|
@ -38,6 +38,7 @@ import { getModelDisplayName, unfurlWildcardModelsInList } from "../key_team_hel
|
|||
import LoggingSettingsView from "../logging_settings_view";
|
||||
import MCPServerSelector from "../mcp_server_management/MCPServerSelector";
|
||||
import MCPToolPermissions from "../mcp_server_management/MCPToolPermissions";
|
||||
import AgentSelector from "../agent_management/AgentSelector";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
import { fetchMCPAccessGroups } from "../networking";
|
||||
import ObjectPermissionsView from "../object_permissions_view";
|
||||
|
|
@ -95,6 +96,8 @@ export interface TeamData {
|
|||
mcp_access_groups?: string[];
|
||||
mcp_tool_permissions?: Record<string, string[]>;
|
||||
vector_stores: string[];
|
||||
agents?: string[];
|
||||
agent_access_groups?: string[];
|
||||
};
|
||||
team_member_budget_table: {
|
||||
max_budget: number;
|
||||
|
|
@ -433,6 +436,19 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
delete values.mcp_servers_and_groups;
|
||||
delete values.mcp_tool_permissions;
|
||||
|
||||
// Handle agent permissions
|
||||
const { agents, accessGroups: agentAccessGroups } = values.agents_and_groups || {
|
||||
agents: [],
|
||||
accessGroups: [],
|
||||
};
|
||||
if (agents && agents.length > 0) {
|
||||
updateData.object_permission.agents = agents;
|
||||
}
|
||||
if (agentAccessGroups && agentAccessGroups.length > 0) {
|
||||
updateData.object_permission.agent_access_groups = agentAccessGroups;
|
||||
}
|
||||
delete values.agents_and_groups;
|
||||
|
||||
const response = await teamUpdateCall(accessToken, updateData);
|
||||
|
||||
NotificationsManager.success("Team settings updated successfully");
|
||||
|
|
@ -630,6 +646,10 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
accessGroups: info.object_permission?.mcp_access_groups || [],
|
||||
},
|
||||
mcp_tool_permissions: info.object_permission?.mcp_tool_permissions || {},
|
||||
agents_and_groups: {
|
||||
agents: info.object_permission?.agents || [],
|
||||
accessGroups: info.object_permission?.agent_access_groups || [],
|
||||
},
|
||||
}}
|
||||
layout="vertical"
|
||||
>
|
||||
|
|
@ -834,6 +854,15 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
)}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Agents / Access Groups" name="agents_and_groups">
|
||||
<AgentSelector
|
||||
onChange={(val) => form.setFieldValue("agents_and_groups", val)}
|
||||
value={form.getFieldValue("agents_and_groups")}
|
||||
accessToken={accessToken || ""}
|
||||
placeholder="Select agents or access groups (optional)"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Organization ID" name="organization_id">
|
||||
<Input type="" disabled />
|
||||
</Form.Item>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { extractLoggingSettings, formatMetadataForDisplay, stripTagsFromMetadata
|
|||
import { KeyResponse } from "../key_team_helpers/key_list";
|
||||
import MCPServerSelector from "../mcp_server_management/MCPServerSelector";
|
||||
import MCPToolPermissions from "../mcp_server_management/MCPToolPermissions";
|
||||
import AgentSelector from "../agent_management/AgentSelector";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
import { fetchMCPAccessGroups, getPromptsList, modelAvailableCall, tagListCall } from "../networking";
|
||||
import { fetchTeamModels } from "../organisms/create_key_button";
|
||||
|
|
@ -175,6 +176,10 @@ export function KeyEditView({
|
|||
accessGroups: keyData.object_permission?.mcp_access_groups || [],
|
||||
},
|
||||
mcp_tool_permissions: keyData.object_permission?.mcp_tool_permissions || {},
|
||||
agents_and_groups: {
|
||||
agents: keyData.object_permission?.agents || [],
|
||||
accessGroups: keyData.object_permission?.agent_access_groups || [],
|
||||
},
|
||||
logging_settings: extractLoggingSettings(keyData.metadata),
|
||||
disabled_callbacks: Array.isArray(keyData.metadata?.litellm_disabled_callbacks)
|
||||
? mapInternalToDisplayNames(keyData.metadata.litellm_disabled_callbacks)
|
||||
|
|
@ -511,6 +516,15 @@ export function KeyEditView({
|
|||
)}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Agents / Access Groups" name="agents_and_groups">
|
||||
<AgentSelector
|
||||
onChange={(val) => form.setFieldValue("agents_and_groups", val)}
|
||||
value={form.getFieldValue("agents_and_groups")}
|
||||
accessToken={accessToken || ""}
|
||||
placeholder="Select agents or access groups (optional)"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Team ID" name="team_id">
|
||||
<Select placeholder="Select team" style={{ width: "100%" }}>
|
||||
{/* Only show All Team Models if team has models */}
|
||||
|
|
|
|||
|
|
@ -144,6 +144,17 @@ export default function KeyInfoView({
|
|||
delete formValues.mcp_tool_permissions;
|
||||
}
|
||||
|
||||
// Handle agent permissions
|
||||
if (formValues.agents_and_groups !== undefined) {
|
||||
const { agents, accessGroups } = formValues.agents_and_groups || { agents: [], accessGroups: [] };
|
||||
formValues.object_permission = {
|
||||
...formValues.object_permission,
|
||||
agents: agents || [],
|
||||
agent_access_groups: accessGroups || [],
|
||||
};
|
||||
delete formValues.agents_and_groups;
|
||||
}
|
||||
|
||||
formValues.max_budget = mapEmptyStringToNull(formValues.max_budget);
|
||||
formValues.tpm_limit = mapEmptyStringToNull(formValues.tpm_limit);
|
||||
formValues.rpm_limit = mapEmptyStringToNull(formValues.rpm_limit);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue