diff --git a/docs/my-website/docs/a2a.md b/docs/my-website/docs/a2a.md index ef19e22dab3..05b147de7cb 100644 --- a/docs/my-website/docs/a2a.md +++ b/docs/my-website/docs/a2a.md @@ -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()) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/components/modals/CreateTeamModal.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/modals/CreateTeamModal.tsx index 80d99b5a9eb..bf9cf92a997 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/teams/components/modals/CreateTeamModal.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/modals/CreateTeamModal.tsx @@ -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 = ({ + + + Agent Settings + + + + Allowed Agents{" "} + + + + + } + name="allowed_agents_and_groups" + className="mt-4" + help="Select agents or access groups this team can access" + > + form.setFieldValue("allowed_agents_and_groups", val)} + value={form.getFieldValue("allowed_agents_and_groups")} + accessToken={accessToken || ""} + placeholder="Select agents or access groups (optional)" + /> + + + + Logging Settings diff --git a/ui/litellm-dashboard/src/components/OldTeams.tsx b/ui/litellm-dashboard/src/components/OldTeams.tsx index 83ec28a5177..b1cc7fcc6aa 100644 --- a/ui/litellm-dashboard/src/components/OldTeams.tsx +++ b/ui/litellm-dashboard/src/components/OldTeams.tsx @@ -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 = ({ 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 = ({ + + + Agent Settings + + + + Allowed Agents{" "} + + + + + } + name="allowed_agents_and_groups" + className="mt-4" + help="Select agents or access groups this team can access" + > + form.setFieldValue("allowed_agents_and_groups", val)} + value={form.getFieldValue("allowed_agents_and_groups")} + accessToken={accessToken || ""} + placeholder="Select agents or access groups (optional)" + /> + + + + Logging Settings diff --git a/ui/litellm-dashboard/src/components/agent_management/AgentSelector.tsx b/ui/litellm-dashboard/src/components/agent_management/AgentSelector.tsx new file mode 100644 index 00000000000..a017004aaca --- /dev/null +++ b/ui/litellm-dashboard/src/components/agent_management/AgentSelector.tsx @@ -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; + agent_card_params?: Record; +} + +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 = ({ + onChange, + value, + className, + accessToken, + placeholder = "Select agents", + disabled = false, +}) => { + const [agents, setAgents] = useState([]); + const [accessGroups, setAccessGroups] = useState([]); + 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(); + 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 ( +
+ +
+ ); +}; + +export default AgentSelector; + diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx b/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx index 695d0571419..6bb014e6187 100644 --- a/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx +++ b/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx @@ -83,6 +83,8 @@ export interface KeyResponse { mcp_access_groups?: string[]; mcp_tool_permissions?: Record; vector_stores: string[]; + agents?: string[]; + agent_access_groups?: string[]; }; auto_rotate?: boolean; rotation_interval?: string; diff --git a/ui/litellm-dashboard/src/components/object_permissions_view.tsx b/ui/litellm-dashboard/src/components/object_permissions_view.tsx index 77727e06602..ac55a57c7c3 100644 --- a/ui/litellm-dashboard/src/components/object_permissions_view.tsx +++ b/ui/litellm-dashboard/src/components/object_permissions_view.tsx @@ -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; 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 = ( -
+
+
); diff --git a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx index 14959e23e5b..3c0a0f520d4 100644 --- a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx +++ b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx @@ -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 = ({ 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 = ({ + + + Agent Settings + + + + Allowed Agents{" "} + + + + + } + name="allowed_agents_and_groups" + help="Select agents or access groups this key can access" + > + form.setFieldValue("allowed_agents_and_groups", val)} + value={form.getFieldValue("allowed_agents_and_groups")} + accessToken={accessToken} + placeholder="Select agents or access groups (optional)" + /> + + + + {premiumUser ? ( diff --git a/ui/litellm-dashboard/src/components/permissions/AgentPermissions.tsx b/ui/litellm-dashboard/src/components/permissions/AgentPermissions.tsx new file mode 100644 index 00000000000..995e25643b4 --- /dev/null +++ b/ui/litellm-dashboard/src/components/permissions/AgentPermissions.tsx @@ -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; + agent_card_params?: Record; +} + +interface AgentPermissionsProps { + agents: string[]; + agentAccessGroups?: string[]; + accessToken?: string | null; +} + +export function AgentPermissions({ + agents, + agentAccessGroups = [], + accessToken +}: AgentPermissionsProps) { + const [agentDetails, setAgentDetails] = useState([]); + + // 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 ( +
+
+ + Agents + + {totalCount} + +
+ + {totalCount > 0 ? ( +
+ {mergedItems.map((item, index) => ( +
+
+
+ {item.type === "agent" ? ( + +
+ + {getAgentDisplayName(item.value)} +
+
+ ) : ( +
+ + {item.value} + + Group + +
+ )} +
+
+
+ ))} +
+ ) : ( +
+ + No agents or access groups configured +
+ )} +
+ ); +} + +export default AgentPermissions; + diff --git a/ui/litellm-dashboard/src/components/team/team_info.tsx b/ui/litellm-dashboard/src/components/team/team_info.tsx index 581722e8fa7..e9e5e6f640d 100644 --- a/ui/litellm-dashboard/src/components/team/team_info.tsx +++ b/ui/litellm-dashboard/src/components/team/team_info.tsx @@ -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; vector_stores: string[]; + agents?: string[]; + agent_access_groups?: string[]; }; team_member_budget_table: { max_budget: number; @@ -433,6 +436,19 @@ const TeamInfoView: React.FC = ({ 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 = ({ 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 = ({ )} + + form.setFieldValue("agents_and_groups", val)} + value={form.getFieldValue("agents_and_groups")} + accessToken={accessToken || ""} + placeholder="Select agents or access groups (optional)" + /> + + diff --git a/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx b/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx index d7376fde146..d655489abf1 100644 --- a/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx @@ -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.setFieldValue("agents_and_groups", val)} + value={form.getFieldValue("agents_and_groups")} + accessToken={accessToken || ""} + placeholder="Select agents or access groups (optional)" + /> + +