mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-12 23:01:41 +00:00
Agents - AI Hub, make agents discoverable on model hub page for internal discovery (#16678)
* feat: initial commit adding agent hub to ui * feat: add viewable agent hub * feat: working support for making both config + db agents public via new 'public_agent_groups' list * fix: agents.py fix types * feat: working PATCH endpoint for UI changes * feat: add new agents panel with working crud * refactor: refactor to show created_at on be/fe * style: align new page with the agents table * style: more style alignment logic * feat: return if agent is public or not in /v1/agents * feat: initial commit adding ui flow for making agents discoverable * feat: new batch make public endpoint * feat(public_model_hub.tsx): show public agents on public model hub table page * fix(public_model_hub.tsx): add code examples for using the agent in a2a * fix: fix indicating if agent has already been made public * docs: document expected spec for agents is A2A * docs: add agent hub docs * docs: document making agents discoverable * docs: add demo video to docs * fix: fix ui linting errors * fix: update tests
This commit is contained in:
parent
e45655a2cf
commit
f36d9e5fd9
34 changed files with 3694 additions and 332 deletions
240
docs/my-website/docs/proxy/ai_hub.md
Normal file
240
docs/my-website/docs/proxy/ai_hub.md
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
import Image from '@theme/IdealImage';
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
# AI Hub
|
||||
|
||||
Share models and agents with your organization. Show developers what's available without needing to rebuild them.
|
||||
|
||||
This feature is **available in v1.74.3-stable and above**.
|
||||
|
||||
## Overview
|
||||
|
||||
Admin can select models/agents to expose on public AI hub → Users go to the public url and see what's available.
|
||||
|
||||
<Image img={require('../../img/final_public_model_hub_view.png')} />
|
||||
|
||||
## Models
|
||||
|
||||
### How to use
|
||||
|
||||
#### 1. Go to the Admin UI
|
||||
|
||||
Navigate to the Model Hub page in the Admin UI (`PROXY_BASE_URL/ui/?login=success&page=model-hub-table`)
|
||||
|
||||
<Image img={require('../../img/model_hub_admin_view.png')} />
|
||||
|
||||
#### 2. Select the models you want to expose
|
||||
|
||||
Click on `Select Models to Make Public` and select the models you want to expose.
|
||||
|
||||
<Image img={require('../../img/make_public_modal.png')} />
|
||||
|
||||
#### 3. Confirm the changes
|
||||
|
||||
<Image img={require('../../img/make_public_modal_confirmation.png')} />
|
||||
|
||||
#### 4. Success!
|
||||
|
||||
Go to the public url (`PROXY_BASE_URL/ui/model_hub_table`) and see available models.
|
||||
|
||||
<Image img={require('../../img/final_public_model_hub_view.png')} />
|
||||
|
||||
### API Endpoints
|
||||
|
||||
- `GET /public/model_hub` – returns the list of public model groups. Requires a valid user API key.
|
||||
- `GET /public/model_hub/info` – returns metadata (docs title, version, useful links) for the public model hub.
|
||||
|
||||
## Agents
|
||||
|
||||
:::info
|
||||
Agents are only available in v1.79.4-stable and above.
|
||||
:::
|
||||
|
||||
Share pre-built agents (A2A spec) across your organization. Users can discover and use agents without rebuilding them.
|
||||
|
||||
[**Demo Video**](https://drive.google.com/file/d/1r-_Rtiu04RW5Fwwu3_eshtA1oZtC3_DH/view?usp=sharing)
|
||||
|
||||
### 1. Create an agent
|
||||
|
||||
Create an agent that follows the [A2A spec](https://a2a.dev/).
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="ui" label="UI">
|
||||
|
||||
<Image img={require('../../img/add_agent.png')} />
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="api" label="API">
|
||||
```bash
|
||||
curl -X POST 'http://0.0.0.0:4000/v1/agents' \
|
||||
--header 'Authorization: Bearer <your-master-key>' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"agent_name": "hello-world-agent",
|
||||
"agent_card_params": {
|
||||
"protocolVersion": "1.0",
|
||||
"name": "Hello World Agent",
|
||||
"description": "Just a hello world agent",
|
||||
"url": "http://localhost:9999/",
|
||||
"version": "1.0.0",
|
||||
"defaultInputModes": ["text"],
|
||||
"defaultOutputModes": ["text"],
|
||||
"capabilities": {
|
||||
"streaming": true
|
||||
},
|
||||
"skills": [
|
||||
{
|
||||
"id": "hello_world",
|
||||
"name": "Returns hello world",
|
||||
"description": "just returns hello world",
|
||||
"tags": ["hello world"],
|
||||
"examples": ["hi", "hello world"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
**Expected Response**
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_id": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"agent_name": "hello-world-agent",
|
||||
"agent_card_params": {
|
||||
"protocolVersion": "1.0",
|
||||
"name": "Hello World Agent",
|
||||
"description": "Just a hello world agent",
|
||||
"url": "http://localhost:9999/",
|
||||
"version": "1.0.0",
|
||||
"defaultInputModes": ["text"],
|
||||
"defaultOutputModes": ["text"],
|
||||
"capabilities": {
|
||||
"streaming": true
|
||||
},
|
||||
"skills": [
|
||||
{
|
||||
"id": "hello_world",
|
||||
"name": "Returns hello world",
|
||||
"description": "just returns hello world",
|
||||
"tags": ["hello world"],
|
||||
"examples": ["hi", "hello world"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"created_at": "2025-11-15T10:30:00Z",
|
||||
"created_by": "user123"
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### 2. Make agent public
|
||||
|
||||
Make the agent discoverable on the AI Hub.
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="ui" label="UI">
|
||||
|
||||
Navigate to the Agents Tab on the AI Hub page
|
||||
|
||||
<Image img={require('../../img/ai_hub_with_agents.png')} />
|
||||
|
||||
Select the agents you want to make public and click on `Make Public` button.
|
||||
|
||||
<Image img={require('../../img/make_agents_public.png')} />
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="api" label="API">
|
||||
|
||||
**Option 1: Make single agent public**
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://0.0.0.0:4000/v1/agents/123e4567-e89b-12d3-a456-426614174000/make_public' \
|
||||
--header 'Authorization: Bearer <your-master-key>' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**Option 2: Make multiple agents public**
|
||||
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://0.0.0.0:4000/v1/agents/make_public' \
|
||||
--header 'Authorization: Bearer <your-master-key>' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"agent_ids": [
|
||||
"123e4567-e89b-12d3-a456-426614174000",
|
||||
"123e4567-e89b-12d3-a456-426614174001"
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
**Expected Response**
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Successfully updated public agent groups",
|
||||
"public_agent_groups": [
|
||||
"123e4567-e89b-12d3-a456-426614174000"
|
||||
],
|
||||
"updated_by": "user123"
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
|
||||
|
||||
### 3. View public agents
|
||||
|
||||
Users can now discover the agent via the public endpoint.
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="ui" label="UI">
|
||||
|
||||
<Image img={require('../../img/public_agent_hub.png')} />
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="api" label="API">
|
||||
|
||||
```bash
|
||||
curl -X GET 'http://0.0.0.0:4000/public/agent_hub' \
|
||||
--header 'Authorization: Bearer <user-api-key>'
|
||||
```
|
||||
|
||||
**Expected Response**
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"protocolVersion": "1.0",
|
||||
"name": "Hello World Agent",
|
||||
"description": "Just a hello world agent",
|
||||
"url": "http://localhost:9999/",
|
||||
"version": "1.0.0",
|
||||
"defaultInputModes": ["text"],
|
||||
"defaultOutputModes": ["text"],
|
||||
"capabilities": {
|
||||
"streaming": true
|
||||
},
|
||||
"skills": [
|
||||
{
|
||||
"id": "hello_world",
|
||||
"name": "Returns hello world",
|
||||
"description": "just returns hello world",
|
||||
"tags": ["hello world"],
|
||||
"examples": ["hi", "hello world"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
|
@ -901,9 +901,11 @@ curl --location 'http://0.0.0.0:4000/chat/completions' \
|
|||
'
|
||||
```
|
||||
|
||||
## Public Model Hub
|
||||
## Public AI Hub
|
||||
|
||||
Share a public page of available models for users
|
||||
Share a public page of available models and agents for users
|
||||
|
||||
[Learn more](./ai_hub.md)
|
||||
|
||||
<Image img={require('../../img/model_hub.png')} style={{ width: '900px', height: 'auto' }}/>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,53 +0,0 @@
|
|||
import Image from '@theme/IdealImage';
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
# Model Hub
|
||||
|
||||
Tell developers what models are available on the proxy.
|
||||
|
||||
This feature is **available in v1.74.3-stable and above**.
|
||||
|
||||
## Overview
|
||||
|
||||
Admin can select models to expose on public model hub -> Users can go to the public url (`/ui/model_hub_table`) and see available models.
|
||||
|
||||
<Image img={require('../../img/final_public_model_hub_view.png')} />
|
||||
|
||||
## How to use
|
||||
|
||||
### 1. Go to the Admin UI
|
||||
|
||||
Navigate to the Model Hub page in the Admin UI (`PROXY_BASE_URL/ui/?login=success&page=model-hub-table`)
|
||||
|
||||
<Image img={require('../../img/model_hub_admin_view.png')} />
|
||||
|
||||
### 2. Select the models you want to expose
|
||||
|
||||
Click on `Make Public` and select the models you want to expose.
|
||||
|
||||
<Image img={require('../../img/make_public_modal.png')} />
|
||||
|
||||
### 3. Confirm the changes
|
||||
|
||||
<Image img={require('../../img/make_public_modal_confirmation.png')} />
|
||||
|
||||
### 4. Success!
|
||||
|
||||
Go to the public url (`PROXY_BASE_URL/ui/model_hub_table`) and see available models.
|
||||
|
||||
<Image img={require('../../img/final_public_model_hub_view.png')} />
|
||||
|
||||
## API Endpoints
|
||||
|
||||
LiteLLM also exposes REST endpoints:
|
||||
|
||||
- `GET /public/model_hub` – returns the list of public model groups. Requires a valid user API key.
|
||||
- `GET /public/model_hub/info` – returns metadata (docs title, version, useful links) for the public model hub.
|
||||
- `GET /public/providers` – returns a sorted list of all providers supported by LiteLLM. No authentication required.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
curl -s PROXY_BASE_URL/public/providers | jq
|
||||
```
|
||||
|
|
@ -59,11 +59,13 @@ Allow others to create/delete their own keys.
|
|||
The Admin UI provides comprehensive model management capabilities:
|
||||
|
||||
- **Add Models**: Add new models through the UI without restarting the proxy
|
||||
- **Model Hub**: Make models public for developers to discover available models
|
||||
- **AI Hub**: Make models and agents public for developers to discover what's available
|
||||
- **Price Data Sync**: Keep model pricing data up to date by syncing from GitHub
|
||||
|
||||
For detailed information on model management, see [Model Management](./model_management.md).
|
||||
|
||||
For information on sharing models and agents, see [AI Hub](./ai_hub.md).
|
||||
|
||||
:::tip Sync Model Pricing Data
|
||||
[Sync model pricing data from GitHub](./sync_models_github.md) to keep your model cost information current.
|
||||
:::
|
||||
|
|
|
|||
BIN
docs/my-website/img/add_agent.png
Normal file
BIN
docs/my-website/img/add_agent.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 616 KiB |
BIN
docs/my-website/img/ai_hub_with_agents.png
Normal file
BIN
docs/my-website/img/ai_hub_with_agents.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 626 KiB |
BIN
docs/my-website/img/make_agents_public.png
Normal file
BIN
docs/my-website/img/make_agents_public.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 647 KiB |
BIN
docs/my-website/img/public_agent_hub.png
Normal file
BIN
docs/my-website/img/public_agent_hub.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 445 KiB |
|
|
@ -149,7 +149,7 @@ const sidebars = {
|
|||
"proxy/admin_ui_sso",
|
||||
"proxy/custom_root_ui",
|
||||
"proxy/custom_sso",
|
||||
"proxy/model_hub",
|
||||
"proxy/ai_hub",
|
||||
"proxy/public_teams",
|
||||
"proxy/self_serve",
|
||||
"proxy/ui",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
-- This is an empty migration.
|
||||
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
-- This is an empty migration.
|
||||
|
||||
|
|
@ -181,22 +181,22 @@ prometheus_initialize_budget_metrics: Optional[bool] = False
|
|||
require_auth_for_metrics_endpoint: Optional[bool] = False
|
||||
argilla_batch_size: Optional[int] = None
|
||||
datadog_use_v1: Optional[bool] = False # if you want to use v1 datadog logged payload.
|
||||
gcs_pub_sub_use_v1: Optional[
|
||||
bool
|
||||
] = False # if you want to use v1 gcs pubsub logged payload
|
||||
generic_api_use_v1: Optional[
|
||||
bool
|
||||
] = False # if you want to use v1 generic api logged payload
|
||||
gcs_pub_sub_use_v1: Optional[bool] = (
|
||||
False # if you want to use v1 gcs pubsub logged payload
|
||||
)
|
||||
generic_api_use_v1: Optional[bool] = (
|
||||
False # if you want to use v1 generic api logged payload
|
||||
)
|
||||
argilla_transformation_object: Optional[Dict[str, Any]] = None
|
||||
_async_input_callback: List[
|
||||
Union[str, Callable, CustomLogger]
|
||||
] = [] # internal variable - async custom callbacks are routed here.
|
||||
_async_success_callback: List[
|
||||
Union[str, Callable, CustomLogger]
|
||||
] = [] # internal variable - async custom callbacks are routed here.
|
||||
_async_failure_callback: List[
|
||||
Union[str, Callable, CustomLogger]
|
||||
] = [] # internal variable - async custom callbacks are routed here.
|
||||
_async_input_callback: List[Union[str, Callable, CustomLogger]] = (
|
||||
[]
|
||||
) # internal variable - async custom callbacks are routed here.
|
||||
_async_success_callback: List[Union[str, Callable, CustomLogger]] = (
|
||||
[]
|
||||
) # internal variable - async custom callbacks are routed here.
|
||||
_async_failure_callback: List[Union[str, Callable, CustomLogger]] = (
|
||||
[]
|
||||
) # internal variable - async custom callbacks are routed here.
|
||||
pre_call_rules: List[Callable] = []
|
||||
post_call_rules: List[Callable] = []
|
||||
turn_off_message_logging: Optional[bool] = False
|
||||
|
|
@ -204,18 +204,18 @@ log_raw_request_response: bool = False
|
|||
redact_messages_in_exceptions: Optional[bool] = False
|
||||
redact_user_api_key_info: Optional[bool] = False
|
||||
filter_invalid_headers: Optional[bool] = False
|
||||
add_user_information_to_llm_headers: Optional[
|
||||
bool
|
||||
] = None # adds user_id, team_id, token hash (params from StandardLoggingMetadata) to request headers
|
||||
add_user_information_to_llm_headers: Optional[bool] = (
|
||||
None # adds user_id, team_id, token hash (params from StandardLoggingMetadata) to request headers
|
||||
)
|
||||
store_audit_logs = False # Enterprise feature, allow users to see audit logs
|
||||
### end of callbacks #############
|
||||
|
||||
email: Optional[
|
||||
str
|
||||
] = None # Not used anymore, will be removed in next MAJOR release - https://github.com/BerriAI/litellm/discussions/648
|
||||
token: Optional[
|
||||
str
|
||||
] = None # Not used anymore, will be removed in next MAJOR release - https://github.com/BerriAI/litellm/discussions/648
|
||||
email: Optional[str] = (
|
||||
None # Not used anymore, will be removed in next MAJOR release - https://github.com/BerriAI/litellm/discussions/648
|
||||
)
|
||||
token: Optional[str] = (
|
||||
None # Not used anymore, will be removed in next MAJOR release - https://github.com/BerriAI/litellm/discussions/648
|
||||
)
|
||||
telemetry = True
|
||||
max_tokens: int = DEFAULT_MAX_TOKENS # OpenAI Defaults
|
||||
drop_params = bool(os.getenv("LITELLM_DROP_PARAMS", False))
|
||||
|
|
@ -271,9 +271,9 @@ use_client: bool = False
|
|||
ssl_verify: Union[str, bool] = True
|
||||
ssl_security_level: Optional[str] = None
|
||||
ssl_certificate: Optional[str] = None
|
||||
ssl_ecdh_curve: Optional[
|
||||
str
|
||||
] = None # Set to 'X25519' to disable PQC and improve performance
|
||||
ssl_ecdh_curve: Optional[str] = (
|
||||
None # Set to 'X25519' to disable PQC and improve performance
|
||||
)
|
||||
disable_streaming_logging: bool = False
|
||||
disable_token_counter: bool = False
|
||||
disable_add_transform_inline_image_block: bool = False
|
||||
|
|
@ -319,20 +319,24 @@ enable_loadbalancing_on_batch_endpoints: Optional[bool] = None
|
|||
enable_caching_on_provider_specific_optional_params: bool = (
|
||||
False # feature-flag for caching on optional params - e.g. 'top_k'
|
||||
)
|
||||
caching: bool = False # Not used anymore, will be removed in next MAJOR release - https://github.com/BerriAI/litellm/discussions/648
|
||||
caching_with_models: bool = False # # Not used anymore, will be removed in next MAJOR release - https://github.com/BerriAI/litellm/discussions/648
|
||||
cache: Optional[
|
||||
Cache
|
||||
] = None # cache object <- use this - https://docs.litellm.ai/docs/caching
|
||||
caching: bool = (
|
||||
False # Not used anymore, will be removed in next MAJOR release - https://github.com/BerriAI/litellm/discussions/648
|
||||
)
|
||||
caching_with_models: bool = (
|
||||
False # # Not used anymore, will be removed in next MAJOR release - https://github.com/BerriAI/litellm/discussions/648
|
||||
)
|
||||
cache: Optional[Cache] = (
|
||||
None # cache object <- use this - https://docs.litellm.ai/docs/caching
|
||||
)
|
||||
default_in_memory_ttl: Optional[float] = None
|
||||
default_redis_ttl: Optional[float] = None
|
||||
default_redis_batch_cache_expiry: Optional[float] = None
|
||||
model_alias_map: Dict[str, str] = {}
|
||||
model_group_settings: Optional["ModelGroupSettings"] = None
|
||||
max_budget: float = 0.0 # set the max budget across all providers
|
||||
budget_duration: Optional[
|
||||
str
|
||||
] = None # proxy only - resets budget after fixed duration. You can set duration as seconds ("30s"), minutes ("30m"), hours ("30h"), days ("30d").
|
||||
budget_duration: Optional[str] = (
|
||||
None # proxy only - resets budget after fixed duration. You can set duration as seconds ("30s"), minutes ("30m"), hours ("30h"), days ("30d").
|
||||
)
|
||||
default_soft_budget: float = (
|
||||
DEFAULT_SOFT_BUDGET # by default all litellm proxy keys have a soft budget of 50.0
|
||||
)
|
||||
|
|
@ -341,7 +345,9 @@ forward_traceparent_to_llm_provider: bool = False
|
|||
|
||||
_current_cost = 0.0 # private variable, used if max budget is set
|
||||
error_logs: Dict = {}
|
||||
add_function_to_prompt: bool = False # if function calling not supported by api, append function call details to system prompt
|
||||
add_function_to_prompt: bool = (
|
||||
False # if function calling not supported by api, append function call details to system prompt
|
||||
)
|
||||
client_session: Optional[httpx.Client] = None
|
||||
aclient_session: Optional[httpx.AsyncClient] = None
|
||||
model_fallbacks: Optional[List] = None # Deprecated for 'litellm.fallbacks'
|
||||
|
|
@ -379,8 +385,11 @@ prometheus_metrics_config: Optional[List] = None
|
|||
disable_add_prefix_to_prompt: bool = (
|
||||
False # used by anthropic, to disable adding prefix to prompt
|
||||
)
|
||||
disable_copilot_system_to_assistant: bool = False # If false (default), converts all 'system' role messages to 'assistant' for GitHub Copilot compatibility. Set to true to disable this behavior.
|
||||
disable_copilot_system_to_assistant: bool = (
|
||||
False # If false (default), converts all 'system' role messages to 'assistant' for GitHub Copilot compatibility. Set to true to disable this behavior.
|
||||
)
|
||||
public_model_groups: Optional[List[str]] = None
|
||||
public_agent_groups: Optional[List[str]] = None
|
||||
public_model_groups_links: Dict[str, str] = {}
|
||||
#### REQUEST PRIORITIZATION #######
|
||||
priority_reservation: Optional[Dict[str, Union[float, PriorityReservationDict]]] = None
|
||||
|
|
@ -390,13 +399,17 @@ priority_reservation_settings: "PriorityReservationSettings" = (
|
|||
|
||||
|
||||
######## Networking Settings ########
|
||||
use_aiohttp_transport: bool = True # Older variable, aiohttp is now the default. use disable_aiohttp_transport instead.
|
||||
use_aiohttp_transport: bool = (
|
||||
True # Older variable, aiohttp is now the default. use disable_aiohttp_transport instead.
|
||||
)
|
||||
aiohttp_trust_env: bool = False # set to true to use HTTP_ Proxy settings
|
||||
disable_aiohttp_transport: bool = False # Set this to true to use httpx instead
|
||||
disable_aiohttp_trust_env: bool = (
|
||||
False # When False, aiohttp will respect HTTP(S)_PROXY env vars
|
||||
)
|
||||
force_ipv4: bool = False # when True, litellm will force ipv4 for all LLM requests. Some users have seen httpx ConnectionError when using ipv6.
|
||||
force_ipv4: bool = (
|
||||
False # when True, litellm will force ipv4 for all LLM requests. Some users have seen httpx ConnectionError when using ipv6.
|
||||
)
|
||||
module_level_aclient = AsyncHTTPHandler(
|
||||
timeout=request_timeout, client_alias="module level aclient"
|
||||
)
|
||||
|
|
@ -410,13 +423,13 @@ fallbacks: Optional[List] = None
|
|||
context_window_fallbacks: Optional[List] = None
|
||||
content_policy_fallbacks: Optional[List] = None
|
||||
allowed_fails: int = 3
|
||||
num_retries_per_request: Optional[
|
||||
int
|
||||
] = None # for the request overall (incl. fallbacks + model retries)
|
||||
num_retries_per_request: Optional[int] = (
|
||||
None # for the request overall (incl. fallbacks + model retries)
|
||||
)
|
||||
####### SECRET MANAGERS #####################
|
||||
secret_manager_client: Optional[
|
||||
Any
|
||||
] = None # list of instantiated key management clients - e.g. azure kv, infisical, etc.
|
||||
secret_manager_client: Optional[Any] = (
|
||||
None # list of instantiated key management clients - e.g. azure kv, infisical, etc.
|
||||
)
|
||||
_google_kms_resource_name: Optional[str] = None
|
||||
_key_management_system: Optional[KeyManagementSystem] = None
|
||||
_key_management_settings: KeyManagementSettings = KeyManagementSettings()
|
||||
|
|
@ -426,9 +439,9 @@ output_parse_pii: bool = False
|
|||
from litellm.litellm_core_utils.get_model_cost_map import get_model_cost_map
|
||||
|
||||
model_cost = get_model_cost_map(url=model_cost_map_url)
|
||||
cost_discount_config: Dict[
|
||||
str, float
|
||||
] = {} # Provider-specific cost discounts {"vertex_ai": 0.05} = 5% discount
|
||||
cost_discount_config: Dict[str, float] = (
|
||||
{}
|
||||
) # Provider-specific cost discounts {"vertex_ai": 0.05} = 5% discount
|
||||
custom_prompt_dict: Dict[str, dict] = {}
|
||||
check_provider_endpoint = False
|
||||
|
||||
|
|
@ -1423,12 +1436,12 @@ from .types.llms.custom_llm import CustomLLMItem
|
|||
from .types.utils import GenericStreamingChunk
|
||||
|
||||
custom_provider_map: List[CustomLLMItem] = []
|
||||
_custom_providers: List[
|
||||
str
|
||||
] = [] # internal helper util, used to track names of custom providers
|
||||
disable_hf_tokenizer_download: Optional[
|
||||
bool
|
||||
] = None # disable huggingface tokenizer download. Defaults to openai clk100
|
||||
_custom_providers: List[str] = (
|
||||
[]
|
||||
) # internal helper util, used to track names of custom providers
|
||||
disable_hf_tokenizer_download: Optional[bool] = (
|
||||
None # disable huggingface tokenizer download. Defaults to openai clk100
|
||||
)
|
||||
global_disable_no_log_param: bool = False
|
||||
|
||||
### CLI UTILITIES ###
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import os
|
||||
from typing import List, Literal
|
||||
|
||||
DEFAULT_HEALTH_CHECK_PROMPT = str(os.getenv("DEFAULT_HEALTH_CHECK_PROMPT", "test from litellm"))
|
||||
DEFAULT_HEALTH_CHECK_PROMPT = str(
|
||||
os.getenv("DEFAULT_HEALTH_CHECK_PROMPT", "test from litellm")
|
||||
)
|
||||
AZURE_DEFAULT_RESPONSES_API_VERSION = str(
|
||||
os.getenv("AZURE_DEFAULT_RESPONSES_API_VERSION", "preview")
|
||||
)
|
||||
|
|
@ -18,7 +20,9 @@ DEFAULT_SQS_FLUSH_INTERVAL_SECONDS = int(
|
|||
DEFAULT_NUM_WORKERS_LITELLM_PROXY = int(
|
||||
os.getenv("DEFAULT_NUM_WORKERS_LITELLM_PROXY", 1)
|
||||
)
|
||||
DYNAMIC_RATE_LIMIT_ERROR_THRESHOLD_PER_MINUTE = int(os.getenv("DYNAMIC_RATE_LIMIT_ERROR_THRESHOLD_PER_MINUTE", 1))
|
||||
DYNAMIC_RATE_LIMIT_ERROR_THRESHOLD_PER_MINUTE = int(
|
||||
os.getenv("DYNAMIC_RATE_LIMIT_ERROR_THRESHOLD_PER_MINUTE", 1)
|
||||
)
|
||||
DEFAULT_SQS_BATCH_SIZE = int(os.getenv("DEFAULT_SQS_BATCH_SIZE", 512))
|
||||
SQS_SEND_MESSAGE_ACTION = "SendMessage"
|
||||
SQS_API_VERSION = "2012-11-05"
|
||||
|
|
@ -85,8 +89,12 @@ MAX_TOKEN_TRIMMING_ATTEMPTS = int(
|
|||
os.getenv("MAX_TOKEN_TRIMMING_ATTEMPTS", 10)
|
||||
) # Maximum number of attempts to trim the message
|
||||
|
||||
RUNWAYML_DEFAULT_API_VERSION = str(os.getenv("RUNWAYML_DEFAULT_API_VERSION", "2024-11-06"))
|
||||
RUNWAYML_POLLING_TIMEOUT = int(os.getenv("RUNWAYML_POLLING_TIMEOUT", 600)) # 10 minutes default for image generation
|
||||
RUNWAYML_DEFAULT_API_VERSION = str(
|
||||
os.getenv("RUNWAYML_DEFAULT_API_VERSION", "2024-11-06")
|
||||
)
|
||||
RUNWAYML_POLLING_TIMEOUT = int(
|
||||
os.getenv("RUNWAYML_POLLING_TIMEOUT", 600)
|
||||
) # 10 minutes default for image generation
|
||||
|
||||
########## Networking constants ##############################################################
|
||||
_DEFAULT_TTL_FOR_HTTPX_CLIENTS = 3600 # 1 hour, re-use the same httpx client for 1 hour
|
||||
|
|
@ -110,22 +118,21 @@ REALTIME_WEBSOCKET_MAX_MESSAGE_SIZE_BYTES = (
|
|||
DEFAULT_SSL_CIPHERS = os.getenv(
|
||||
"LITELLM_SSL_CIPHERS",
|
||||
# Priority 1: TLS 1.3 ciphers (fastest, ~50ms handshake)
|
||||
"TLS_AES_256_GCM_SHA384:" # Fastest observed in testing
|
||||
"TLS_AES_128_GCM_SHA256:" # Slightly faster than 256-bit
|
||||
"TLS_CHACHA20_POLY1305_SHA256:" # Fast on ARM/mobile
|
||||
"TLS_AES_256_GCM_SHA384:" # Fastest observed in testing
|
||||
"TLS_AES_128_GCM_SHA256:" # Slightly faster than 256-bit
|
||||
"TLS_CHACHA20_POLY1305_SHA256:" # Fast on ARM/mobile
|
||||
# Priority 2: TLS 1.2 ECDHE+GCM (fast, ~100ms handshake, widely supported)
|
||||
"ECDHE-RSA-AES256-GCM-SHA384:"
|
||||
"ECDHE-RSA-AES128-GCM-SHA256:"
|
||||
"ECDHE-ECDSA-AES256-GCM-SHA384:"
|
||||
"ECDHE-ECDSA-AES128-GCM-SHA256:"
|
||||
# Priority 3: Additional modern ciphers (good balance)
|
||||
"ECDHE-RSA-CHACHA20-POLY1305:"
|
||||
"ECDHE-ECDSA-CHACHA20-POLY1305:"
|
||||
"ECDHE-RSA-CHACHA20-POLY1305:" "ECDHE-ECDSA-CHACHA20-POLY1305:"
|
||||
# Priority 4: Widely compatible fallbacks (slower but universally supported)
|
||||
"ECDHE-RSA-AES256-SHA384:" # Common fallback
|
||||
"ECDHE-RSA-AES128-SHA256:" # Very widely supported
|
||||
"AES256-GCM-SHA384:" # Non-PFS fallback (compatibility)
|
||||
"AES128-GCM-SHA256", # Last resort (maximum compatibility)
|
||||
"ECDHE-RSA-AES256-SHA384:" # Common fallback
|
||||
"ECDHE-RSA-AES128-SHA256:" # Very widely supported
|
||||
"AES256-GCM-SHA384:" # Non-PFS fallback (compatibility)
|
||||
"AES128-GCM-SHA256", # Last resort (maximum compatibility)
|
||||
)
|
||||
|
||||
########### v2 Architecture constants for managing writing updates to the database ###########
|
||||
|
|
@ -282,7 +289,9 @@ ANTHROPIC_WEB_SEARCH_TOOL_MAX_USES = {
|
|||
DEFAULT_IMAGE_ENDPOINT_MODEL = "dall-e-2"
|
||||
DEFAULT_VIDEO_ENDPOINT_MODEL = "sora-2"
|
||||
|
||||
DEFAULT_GOOGLE_VIDEO_DURATION_SECONDS = int(os.getenv("DEFAULT_GOOGLE_VIDEO_DURATION_SECONDS", 8))
|
||||
DEFAULT_GOOGLE_VIDEO_DURATION_SECONDS = int(
|
||||
os.getenv("DEFAULT_GOOGLE_VIDEO_DURATION_SECONDS", 8)
|
||||
)
|
||||
|
||||
### DATAFORSEO CONSTANTS ###
|
||||
DEFAULT_DATAFORSEO_LOCATION_CODE = int(
|
||||
|
|
@ -371,7 +380,7 @@ LITELLM_CHAT_PROVIDERS = [
|
|||
"vercel_ai_gateway",
|
||||
"wandb",
|
||||
"ovhcloud",
|
||||
"lemonade"
|
||||
"lemonade",
|
||||
]
|
||||
|
||||
LITELLM_EMBEDDING_PROVIDERS_SUPPORTING_INPUT_ARRAY_OF_TOKENS = [
|
||||
|
|
@ -631,7 +640,7 @@ clarifai_models: set = set(
|
|||
"clarifai/qwen.qwenLM.Qwen3-14B",
|
||||
"clarifai/qwen.qwenLM.QwQ-32B-AWQ",
|
||||
"clarifai/anthropic.completion.claude-3_5-haiku",
|
||||
"clarifai/anthropic.completion.claude-3_7-sonnet",
|
||||
"clarifai/anthropic.completion.claude-3_7-sonnet",
|
||||
]
|
||||
)
|
||||
|
||||
|
|
@ -797,28 +806,22 @@ WANDB_MODELS: set = set(
|
|||
# openai models
|
||||
"openai/gpt-oss-120b",
|
||||
"openai/gpt-oss-20b",
|
||||
|
||||
# zai-org models
|
||||
"zai-org/GLM-4.5",
|
||||
|
||||
# Qwen models
|
||||
"Qwen/Qwen3-235B-A22B-Instruct-2507",
|
||||
"Qwen/Qwen3-Coder-480B-A35B-Instruct",
|
||||
"Qwen/Qwen3-235B-A22B-Thinking-2507",
|
||||
|
||||
# moonshotai
|
||||
"moonshotai/Kimi-K2-Instruct",
|
||||
|
||||
# meta models
|
||||
"meta-llama/Llama-3.1-8B-Instruct",
|
||||
"meta-llama/Llama-3.3-70B-Instruct",
|
||||
"meta-llama/Llama-4-Scout-17B-16E-Instruct",
|
||||
|
||||
# deepseek-ai
|
||||
"deepseek-ai/DeepSeek-V3.1",
|
||||
"deepseek-ai/DeepSeek-R1-0528",
|
||||
"deepseek-ai/DeepSeek-V3-0324",
|
||||
|
||||
# microsoft
|
||||
"microsoft/Phi-4-mini-instruct",
|
||||
]
|
||||
|
|
@ -1032,7 +1035,9 @@ LITELLM_INTERNAL_JOBS_SERVICE_ACCOUNT_NAME = "litellm_internal_jobs"
|
|||
|
||||
# Key Rotation Constants
|
||||
LITELLM_KEY_ROTATION_ENABLED = os.getenv("LITELLM_KEY_ROTATION_ENABLED", "false")
|
||||
LITELLM_KEY_ROTATION_CHECK_INTERVAL_SECONDS = int(os.getenv("LITELLM_KEY_ROTATION_CHECK_INTERVAL_SECONDS", 86400)) # 24 hours default
|
||||
LITELLM_KEY_ROTATION_CHECK_INTERVAL_SECONDS = int(
|
||||
os.getenv("LITELLM_KEY_ROTATION_CHECK_INTERVAL_SECONDS", 86400)
|
||||
) # 24 hours default
|
||||
UI_SESSION_TOKEN_TEAM_ID = "litellm-dashboard"
|
||||
LITELLM_PROXY_ADMIN_NAME = "default_user_id"
|
||||
|
||||
|
|
@ -1060,14 +1065,28 @@ PROXY_BATCH_POLLING_INTERVAL = int(os.getenv("PROXY_BATCH_POLLING_INTERVAL", 360
|
|||
PROXY_BUDGET_RESCHEDULER_MAX_TIME = int(
|
||||
os.getenv("PROXY_BUDGET_RESCHEDULER_MAX_TIME", 605)
|
||||
)
|
||||
PROXY_BATCH_WRITE_AT = int(os.getenv("PROXY_BATCH_WRITE_AT", 10)) # in seconds, increased from 10
|
||||
PROXY_BATCH_WRITE_AT = int(
|
||||
os.getenv("PROXY_BATCH_WRITE_AT", 10)
|
||||
) # in seconds, increased from 10
|
||||
|
||||
# APScheduler Configuration - MEMORY LEAK FIX
|
||||
# These settings prevent memory leaks in APScheduler's normalize() and _apply_jitter() functions
|
||||
APSCHEDULER_COALESCE = os.getenv("APSCHEDULER_COALESCE", "True").lower() in ["true", "1"] # collapse many missed runs into one
|
||||
APSCHEDULER_MISFIRE_GRACE_TIME = int(os.getenv("APSCHEDULER_MISFIRE_GRACE_TIME", 3600)) # ignore runs older than 1 hour (was 120)
|
||||
APSCHEDULER_MAX_INSTANCES = int(os.getenv("APSCHEDULER_MAX_INSTANCES", 1)) # prevent concurrent job instances
|
||||
APSCHEDULER_REPLACE_EXISTING = os.getenv("APSCHEDULER_REPLACE_EXISTING", "True").lower() in ["true", "1"] # always replace existing jobs
|
||||
APSCHEDULER_COALESCE = os.getenv("APSCHEDULER_COALESCE", "True").lower() in [
|
||||
"true",
|
||||
"1",
|
||||
] # collapse many missed runs into one
|
||||
APSCHEDULER_MISFIRE_GRACE_TIME = int(
|
||||
os.getenv("APSCHEDULER_MISFIRE_GRACE_TIME", 3600)
|
||||
) # ignore runs older than 1 hour (was 120)
|
||||
APSCHEDULER_MAX_INSTANCES = int(
|
||||
os.getenv("APSCHEDULER_MAX_INSTANCES", 1)
|
||||
) # prevent concurrent job instances
|
||||
APSCHEDULER_REPLACE_EXISTING = os.getenv(
|
||||
"APSCHEDULER_REPLACE_EXISTING", "True"
|
||||
).lower() in [
|
||||
"true",
|
||||
"1",
|
||||
] # always replace existing jobs
|
||||
|
||||
DEFAULT_HEALTH_CHECK_INTERVAL = int(
|
||||
os.getenv("DEFAULT_HEALTH_CHECK_INTERVAL", 300)
|
||||
|
|
@ -1097,6 +1116,7 @@ SECRET_MANAGER_REFRESH_INTERVAL = int(
|
|||
)
|
||||
LITELLM_SETTINGS_SAFE_DB_OVERRIDES = [
|
||||
"default_internal_user_params",
|
||||
"public_agent_groups",
|
||||
"public_model_groups",
|
||||
"public_model_groups_links",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -34,4 +34,4 @@ agent_list:
|
|||
make_public: true
|
||||
|
||||
litellm_settings:
|
||||
callbacks: ["prometheus"]
|
||||
callbacks: ["prometheus"]
|
||||
|
|
|
|||
|
|
@ -1,42 +1,50 @@
|
|||
import hashlib
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import litellm
|
||||
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
|
||||
from litellm.proxy.utils import PrismaClient
|
||||
from litellm.types.agents import AgentConfig
|
||||
from litellm.types.agents import AgentConfig, AgentResponse, PatchAgentRequest
|
||||
|
||||
|
||||
class AgentRegistry:
|
||||
def __init__(self):
|
||||
self.agent_list: List[AgentConfig] = []
|
||||
self.agent_list: List[AgentResponse] = []
|
||||
|
||||
def reset_agent_list(self):
|
||||
self.agent_list = []
|
||||
|
||||
def register_agent(self, agent_config: AgentConfig):
|
||||
def register_agent(self, agent_config: AgentResponse):
|
||||
self.agent_list.append(agent_config)
|
||||
|
||||
def deregister_agent(self, agent_name: str):
|
||||
self.agent_list = [
|
||||
agent for agent in self.agent_list if agent.get("agent_name") != agent_name
|
||||
agent for agent in self.agent_list if agent.agent_name != agent_name
|
||||
]
|
||||
|
||||
def get_agent_list(self, agent_names: Optional[List[str]] = None):
|
||||
if agent_names is not None:
|
||||
return [
|
||||
agent
|
||||
for agent in self.agent_list
|
||||
if agent.get("agent_name") in agent_names
|
||||
agent for agent in self.agent_list if agent.agent_name in agent_names
|
||||
]
|
||||
return self.agent_list
|
||||
|
||||
def get_public_agent_list(self):
|
||||
public_agent_list = []
|
||||
def get_public_agent_list(self) -> List[AgentResponse]:
|
||||
public_agent_list: List[AgentResponse] = []
|
||||
if litellm.public_agent_groups is None:
|
||||
return public_agent_list
|
||||
for agent in self.agent_list:
|
||||
if agent.get("litellm_params", {}).get("make_public", False) is True:
|
||||
if agent.agent_id in litellm.public_agent_groups:
|
||||
public_agent_list.append(agent)
|
||||
return public_agent_list
|
||||
|
||||
def _create_agent_id(self, agent_config: AgentConfig) -> str:
|
||||
return hashlib.sha256(
|
||||
json.dumps(agent_config, sort_keys=True).encode()
|
||||
).hexdigest()
|
||||
|
||||
def load_agents_from_config(self, agent_config: Optional[List[AgentConfig]] = None):
|
||||
if agent_config is None:
|
||||
return None
|
||||
|
|
@ -50,7 +58,10 @@ class AgentRegistry:
|
|||
if not all([agent_name, agent_card_params]):
|
||||
continue
|
||||
|
||||
self.register_agent(agent_config=agent_config_item)
|
||||
# create a stable hash id for config item
|
||||
config_hash = self._create_agent_id(agent_config_item)
|
||||
|
||||
self.register_agent(agent_config=AgentResponse(agent_id=config_hash, **agent_config_item)) # type: ignore
|
||||
|
||||
def load_agents_from_db_and_config(
|
||||
self,
|
||||
|
|
@ -64,14 +75,14 @@ class AgentRegistry:
|
|||
if not isinstance(agent_config_item, dict):
|
||||
raise ValueError("agent_config must be a list of dictionaries")
|
||||
|
||||
self.register_agent(agent_config=agent_config_item)
|
||||
self.register_agent(agent_config=AgentResponse(agent_id=self._create_agent_id(agent_config_item), **agent_config_item)) # type: ignore
|
||||
|
||||
if db_agents:
|
||||
for db_agent in db_agents:
|
||||
if not isinstance(db_agent, dict):
|
||||
raise ValueError("db_agents must be a list of dictionaries")
|
||||
|
||||
self.register_agent(agent_config=AgentConfig(**db_agent)) # type: ignore
|
||||
self.register_agent(agent_config=AgentResponse(**db_agent)) # type: ignore
|
||||
return self.agent_list
|
||||
|
||||
###########################################################
|
||||
|
|
@ -79,7 +90,7 @@ class AgentRegistry:
|
|||
############################################################
|
||||
async def add_agent_to_db(
|
||||
self, agent: AgentConfig, prisma_client: PrismaClient, created_by: str
|
||||
) -> Dict[str, Any]:
|
||||
) -> AgentResponse:
|
||||
"""
|
||||
Add an agent to the database
|
||||
"""
|
||||
|
|
@ -119,7 +130,7 @@ class AgentRegistry:
|
|||
}
|
||||
)
|
||||
|
||||
return dict(created_agent)
|
||||
return AgentResponse(**created_agent.model_dump()) # type: ignore
|
||||
except Exception as e:
|
||||
raise Exception(f"Error adding agent to DB: {str(e)}")
|
||||
|
||||
|
|
@ -137,13 +148,70 @@ class AgentRegistry:
|
|||
except Exception as e:
|
||||
raise Exception(f"Error deleting agent from DB: {str(e)}")
|
||||
|
||||
async def patch_agent_in_db(
|
||||
self,
|
||||
agent_id: str,
|
||||
agent: PatchAgentRequest,
|
||||
prisma_client: PrismaClient,
|
||||
updated_by: str,
|
||||
) -> AgentResponse:
|
||||
"""
|
||||
Patch an agent in the database.
|
||||
|
||||
Get the existing agent from the database and patch it with the new values.
|
||||
|
||||
Args:
|
||||
agent_id: The ID of the agent to patch
|
||||
agent: The new agent values to patch
|
||||
prisma_client: The Prisma client to use
|
||||
updated_by: The user ID of the user who is patching the agent
|
||||
|
||||
Returns:
|
||||
The patched agent
|
||||
"""
|
||||
try:
|
||||
|
||||
existing_agent = await prisma_client.db.litellm_agentstable.find_unique(
|
||||
where={"agent_id": agent_id}
|
||||
)
|
||||
if existing_agent is not None:
|
||||
existing_agent = dict(existing_agent)
|
||||
|
||||
if existing_agent is None:
|
||||
raise Exception(f"Agent with ID {agent_id} not found")
|
||||
|
||||
augment_agent = {**existing_agent, **agent}
|
||||
update_data = {}
|
||||
if augment_agent.get("agent_name"):
|
||||
update_data["agent_name"] = augment_agent.get("agent_name")
|
||||
if augment_agent.get("litellm_params"):
|
||||
update_data["litellm_params"] = safe_dumps(
|
||||
augment_agent.get("litellm_params")
|
||||
)
|
||||
if augment_agent.get("agent_card_params"):
|
||||
update_data["agent_card_params"] = safe_dumps(
|
||||
augment_agent.get("agent_card_params")
|
||||
)
|
||||
# Patch agent in DB
|
||||
patched_agent = await prisma_client.db.litellm_agentstable.update(
|
||||
where={"agent_id": agent_id},
|
||||
data={
|
||||
**update_data,
|
||||
"updated_by": updated_by,
|
||||
"updated_at": datetime.now(timezone.utc),
|
||||
},
|
||||
)
|
||||
return AgentResponse(**patched_agent.model_dump()) # type: ignore
|
||||
except Exception as e:
|
||||
raise Exception(f"Error patching agent in DB: {str(e)}")
|
||||
|
||||
async def update_agent_in_db(
|
||||
self,
|
||||
agent_id: str,
|
||||
agent: AgentConfig,
|
||||
prisma_client: PrismaClient,
|
||||
updated_by: str,
|
||||
) -> Dict[str, Any]:
|
||||
) -> AgentResponse:
|
||||
"""
|
||||
Update an agent in the database
|
||||
"""
|
||||
|
|
@ -182,7 +250,7 @@ class AgentRegistry:
|
|||
},
|
||||
)
|
||||
|
||||
return dict(updated_agent)
|
||||
return AgentResponse(**updated_agent.model_dump()) # type: ignore
|
||||
except Exception as e:
|
||||
raise Exception(f"Error updating agent in DB: {str(e)}")
|
||||
|
||||
|
|
@ -209,27 +277,27 @@ class AgentRegistry:
|
|||
def get_agent_by_id(
|
||||
self,
|
||||
agent_id: str,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
) -> Optional[AgentResponse]:
|
||||
"""
|
||||
Get an agent by its ID from the database
|
||||
"""
|
||||
try:
|
||||
for agent in self.agent_list:
|
||||
if agent.get("agent_id") == agent_id:
|
||||
return dict(agent)
|
||||
if agent.agent_id == agent_id:
|
||||
return agent
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
raise Exception(f"Error getting agent from DB: {str(e)}")
|
||||
|
||||
def get_agent_by_name(self, agent_name: str) -> Optional[Dict[str, Any]]:
|
||||
def get_agent_by_name(self, agent_name: str) -> Optional[AgentResponse]:
|
||||
"""
|
||||
Get an agent by its name from the database
|
||||
"""
|
||||
try:
|
||||
for agent in self.agent_list:
|
||||
if agent.get("agent_name") == agent_name:
|
||||
return dict(agent)
|
||||
if agent.agent_name == agent_name:
|
||||
return agent
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -12,10 +12,17 @@ from typing import Any, List
|
|||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
|
||||
import litellm
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.proxy._types import CommonProxyErrors, LitellmUserRoles, UserAPIKeyAuth
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
from litellm.types.agents import AgentConfig, AgentResponse
|
||||
from litellm.types.agents import (
|
||||
AgentConfig,
|
||||
AgentMakePublicResponse,
|
||||
AgentResponse,
|
||||
MakeAgentsPublicRequest,
|
||||
PatchAgentRequest,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
|
@ -24,7 +31,7 @@ router = APIRouter()
|
|||
"/v1/agents",
|
||||
tags=["[beta] Agents"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=List[AgentConfig],
|
||||
response_model=List[AgentResponse],
|
||||
)
|
||||
async def get_agents(
|
||||
request: Request,
|
||||
|
|
@ -38,25 +45,40 @@ async def get_agents(
|
|||
-H "Authorization: Bearer your-key" \
|
||||
```
|
||||
|
||||
Returns: List[AgentConfig]
|
||||
Returns: List[AgentResponse]
|
||||
|
||||
"""
|
||||
from litellm.proxy.agent_endpoints.agent_registry import global_agent_registry
|
||||
|
||||
try:
|
||||
returned_agents: List[AgentResponse] = []
|
||||
if (
|
||||
user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN
|
||||
or user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value
|
||||
):
|
||||
return global_agent_registry.get_agent_list()
|
||||
returned_agents = global_agent_registry.get_agent_list()
|
||||
key_agents = user_api_key_dict.metadata.get("agents")
|
||||
_team_metadata = user_api_key_dict.team_metadata or {}
|
||||
team_agents = _team_metadata.get("agents")
|
||||
if key_agents is not None:
|
||||
return global_agent_registry.get_agent_list(agent_names=key_agents)
|
||||
returned_agents = global_agent_registry.get_agent_list(
|
||||
agent_names=key_agents
|
||||
)
|
||||
if team_agents is not None:
|
||||
return global_agent_registry.get_agent_list(agent_names=team_agents)
|
||||
return []
|
||||
returned_agents = global_agent_registry.get_agent_list(
|
||||
agent_names=team_agents
|
||||
)
|
||||
|
||||
# add is_public field to each agent - we do it this way, to allow setting config agents as public
|
||||
for agent in returned_agents:
|
||||
if agent.litellm_params is None:
|
||||
agent.litellm_params = {}
|
||||
agent.litellm_params["is_public"] = (
|
||||
litellm.public_agent_groups is not None
|
||||
and (agent.agent_id in litellm.public_agent_groups)
|
||||
)
|
||||
|
||||
return returned_agents
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
|
|
@ -149,12 +171,12 @@ async def create_agent(
|
|||
agent=request, prisma_client=prisma_client, created_by=created_by
|
||||
)
|
||||
|
||||
agent_name = result.get("agent_name", "Unknown")
|
||||
agent_id = result.get("agent_id", "Unknown")
|
||||
agent_name = result.agent_name
|
||||
agent_id = result.agent_id
|
||||
|
||||
# Also register in memory
|
||||
try:
|
||||
AGENT_REGISTRY.register_agent(agent_config=request)
|
||||
AGENT_REGISTRY.register_agent(agent_config=result)
|
||||
verbose_proxy_logger.info(
|
||||
f"Successfully registered agent '{agent_name}' (ID: {agent_id}) in memory"
|
||||
)
|
||||
|
|
@ -163,7 +185,7 @@ async def create_agent(
|
|||
f"Failed to register agent '{agent_name}' (ID: {agent_id}) in memory: {reg_error}"
|
||||
)
|
||||
|
||||
return AgentResponse(**result)
|
||||
return result
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
|
|
@ -200,14 +222,14 @@ async def get_agent_by_id(agent_id: str):
|
|||
where={"agent_id": agent_id}
|
||||
)
|
||||
if agent is not None:
|
||||
agent = dict(agent)
|
||||
agent = AgentResponse(**agent.model_dump()) # type: ignore
|
||||
|
||||
if agent is None:
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Agent with ID {agent_id} not found"
|
||||
)
|
||||
|
||||
return AgentResponse(**agent)
|
||||
return agent
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
|
|
@ -290,13 +312,102 @@ async def update_agent(
|
|||
# deregister in memory
|
||||
AGENT_REGISTRY.deregister_agent(agent_name=existing_agent.get("agent_name")) # type: ignore
|
||||
# register in memory
|
||||
AGENT_REGISTRY.register_agent(agent_config=request)
|
||||
AGENT_REGISTRY.register_agent(agent_config=result)
|
||||
|
||||
verbose_proxy_logger.info(
|
||||
f"Successfully updated agent '{existing_agent.get('agent_name')}' (ID: {agent_id}) in memory"
|
||||
)
|
||||
|
||||
return AgentResponse(**result)
|
||||
return result
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error updating agent: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/v1/agents/{agent_id}",
|
||||
tags=["[beta] Agents"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=AgentResponse,
|
||||
)
|
||||
async def patch_agent(
|
||||
agent_id: str,
|
||||
request: PatchAgentRequest,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
Update an existing agent
|
||||
|
||||
Example Request:
|
||||
```bash
|
||||
curl -X PUT "http://localhost:4000/agents/123e4567-e89b-12d3-a456-426614174000" \\
|
||||
-H "Authorization: Bearer <your_api_key>" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"agent": {
|
||||
"agent_name": "updated-agent",
|
||||
"agent_card_params": {
|
||||
"protocolVersion": "1.0",
|
||||
"name": "Updated Agent",
|
||||
"description": "Updated description",
|
||||
"url": "http://localhost:9999/",
|
||||
"version": "1.1.0",
|
||||
"defaultInputModes": ["text"],
|
||||
"defaultOutputModes": ["text"],
|
||||
"capabilities": {
|
||||
"streaming": true
|
||||
},
|
||||
"skills": []
|
||||
},
|
||||
"litellm_params": {
|
||||
"make_public": false
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
"""
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(
|
||||
status_code=500, detail=CommonProxyErrors.db_not_connected_error.value
|
||||
)
|
||||
|
||||
try:
|
||||
# Check if agent exists
|
||||
existing_agent = await prisma_client.db.litellm_agentstable.find_unique(
|
||||
where={"agent_id": agent_id}
|
||||
)
|
||||
if existing_agent is not None:
|
||||
existing_agent = dict(existing_agent)
|
||||
|
||||
if existing_agent is None:
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Agent with ID {agent_id} not found"
|
||||
)
|
||||
|
||||
# Get the user ID from the API key auth
|
||||
updated_by = user_api_key_dict.user_id or "unknown"
|
||||
|
||||
result = await AGENT_REGISTRY.patch_agent_in_db(
|
||||
agent_id=agent_id,
|
||||
agent=request,
|
||||
prisma_client=prisma_client,
|
||||
updated_by=updated_by,
|
||||
)
|
||||
|
||||
# deregister in memory
|
||||
AGENT_REGISTRY.deregister_agent(agent_name=existing_agent.get("agent_name")) # type: ignore
|
||||
# register in memory
|
||||
AGENT_REGISTRY.register_agent(agent_config=result)
|
||||
|
||||
verbose_proxy_logger.info(
|
||||
f"Successfully updated agent '{existing_agent.get('agent_name')}' (ID: {agent_id}) in memory"
|
||||
)
|
||||
|
||||
return result
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
|
|
@ -356,3 +467,229 @@ async def delete_agent(agent_id: str):
|
|||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error deleting agent: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post(
|
||||
"/v1/agents/{agent_id}/make_public",
|
||||
tags=["[beta] Agents"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=AgentMakePublicResponse,
|
||||
)
|
||||
async def make_agent_public(
|
||||
agent_id: str,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
Make an agent publicly discoverable
|
||||
|
||||
Example Request:
|
||||
```bash
|
||||
curl -X POST "http://localhost:4000/v1/agents/123e4567-e89b-12d3-a456-426614174000/make_public" \\
|
||||
-H "Authorization: Bearer <your_api_key>" \\
|
||||
-H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
Example Response:
|
||||
```json
|
||||
{
|
||||
"agent_id": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"agent_name": "my-custom-agent",
|
||||
"litellm_params": {
|
||||
"make_public": true
|
||||
},
|
||||
"agent_card_params": {...},
|
||||
"created_at": "2025-11-15T10:30:00Z",
|
||||
"updated_at": "2025-11-15T10:35:00Z",
|
||||
"created_by": "user123",
|
||||
"updated_by": "user123"
|
||||
}
|
||||
```
|
||||
"""
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(
|
||||
status_code=500, detail=CommonProxyErrors.db_not_connected_error.value
|
||||
)
|
||||
|
||||
try:
|
||||
# Update the public model groups
|
||||
import litellm
|
||||
from litellm.proxy.agent_endpoints.agent_registry import (
|
||||
global_agent_registry as AGENT_REGISTRY,
|
||||
)
|
||||
from litellm.proxy.proxy_server import proxy_config
|
||||
|
||||
# Check if user has admin permissions
|
||||
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail={
|
||||
"error": "Only proxy admins can update public model groups. Your role={}".format(
|
||||
user_api_key_dict.user_role
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
agent = AGENT_REGISTRY.get_agent_by_id(agent_id=agent_id)
|
||||
if agent is None:
|
||||
# check if agent exists in DB
|
||||
agent = await prisma_client.db.litellm_agentstable.find_unique(
|
||||
where={"agent_id": agent_id}
|
||||
)
|
||||
if agent is not None:
|
||||
agent = AgentResponse(**agent.model_dump()) # type: ignore
|
||||
|
||||
if agent is None:
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Agent with ID {agent_id} not found"
|
||||
)
|
||||
|
||||
if litellm.public_agent_groups is None:
|
||||
litellm.public_agent_groups = []
|
||||
# handle duplicates
|
||||
if agent.agent_id in litellm.public_agent_groups:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Agent with name {agent.agent_name} already in public agent groups",
|
||||
)
|
||||
litellm.public_agent_groups.append(agent.agent_id)
|
||||
|
||||
# Load existing config
|
||||
config = await proxy_config.get_config()
|
||||
|
||||
# Update config with new settings
|
||||
if "litellm_settings" not in config or config["litellm_settings"] is None:
|
||||
config["litellm_settings"] = {}
|
||||
|
||||
config["litellm_settings"]["public_agent_groups"] = litellm.public_agent_groups
|
||||
|
||||
# Save the updated config
|
||||
await proxy_config.save_config(new_config=config)
|
||||
|
||||
verbose_proxy_logger.debug(
|
||||
f"Updated public agent groups to: {litellm.public_agent_groups} by user: {user_api_key_dict.user_id}"
|
||||
)
|
||||
|
||||
return {
|
||||
"message": "Successfully updated public agent groups",
|
||||
"public_agent_groups": litellm.public_agent_groups,
|
||||
"updated_by": user_api_key_dict.user_id,
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error making agent public: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post(
|
||||
"/v1/agents/make_public",
|
||||
tags=["[beta] Agents"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=AgentMakePublicResponse,
|
||||
)
|
||||
async def make_agents_public(
|
||||
request: MakeAgentsPublicRequest,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
Make multiple agents publicly discoverable
|
||||
|
||||
Example Request:
|
||||
```bash
|
||||
curl -X POST "http://localhost:4000/v1/agents/make_public" \\
|
||||
-H "Authorization: Bearer <your_api_key>" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"agent_ids": ["123e4567-e89b-12d3-a456-426614174000", "123e4567-e89b-12d3-a456-426614174001"]
|
||||
}'
|
||||
```
|
||||
|
||||
Example Response:
|
||||
```json
|
||||
{
|
||||
"agent_id": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"agent_name": "my-custom-agent",
|
||||
"litellm_params": {
|
||||
"make_public": true
|
||||
},
|
||||
"agent_card_params": {...},
|
||||
"created_at": "2025-11-15T10:30:00Z",
|
||||
"updated_at": "2025-11-15T10:35:00Z",
|
||||
"created_by": "user123",
|
||||
"updated_by": "user123"
|
||||
}
|
||||
```
|
||||
"""
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(
|
||||
status_code=500, detail=CommonProxyErrors.db_not_connected_error.value
|
||||
)
|
||||
|
||||
try:
|
||||
# Update the public model groups
|
||||
import litellm
|
||||
from litellm.proxy.agent_endpoints.agent_registry import (
|
||||
global_agent_registry as AGENT_REGISTRY,
|
||||
)
|
||||
from litellm.proxy.proxy_server import proxy_config
|
||||
|
||||
# Load existing config
|
||||
config = await proxy_config.get_config()
|
||||
# Check if user has admin permissions
|
||||
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail={
|
||||
"error": "Only proxy admins can update public model groups. Your role={}".format(
|
||||
user_api_key_dict.user_role
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
if litellm.public_agent_groups is None:
|
||||
litellm.public_agent_groups = []
|
||||
|
||||
for agent_id in request.agent_ids:
|
||||
agent = AGENT_REGISTRY.get_agent_by_id(agent_id=agent_id)
|
||||
if agent is None:
|
||||
# check if agent exists in DB
|
||||
agent = await prisma_client.db.litellm_agentstable.find_unique(
|
||||
where={"agent_id": agent_id}
|
||||
)
|
||||
if agent is not None:
|
||||
agent = AgentResponse(**agent.model_dump()) # type: ignore
|
||||
|
||||
if agent is None:
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Agent with ID {agent_id} not found"
|
||||
)
|
||||
|
||||
litellm.public_agent_groups = request.agent_ids
|
||||
|
||||
# Update config with new settings
|
||||
if "litellm_settings" not in config or config["litellm_settings"] is None:
|
||||
config["litellm_settings"] = {}
|
||||
|
||||
config["litellm_settings"]["public_agent_groups"] = litellm.public_agent_groups
|
||||
|
||||
# Save the updated config
|
||||
await proxy_config.save_config(new_config=config)
|
||||
|
||||
verbose_proxy_logger.debug(
|
||||
f"Updated public agent groups to: {litellm.public_agent_groups} by user: {user_api_key_dict.user_id}"
|
||||
)
|
||||
|
||||
return {
|
||||
"message": "Successfully updated public agent groups",
|
||||
"public_agent_groups": litellm.public_agent_groups,
|
||||
"updated_by": user_api_key_dict.user_id,
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error making agent public: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
|
|
|||
|
|
@ -1911,7 +1911,9 @@ class ProxyConfig:
|
|||
raise Exception("Unable to load config from given source.")
|
||||
else:
|
||||
# default to file
|
||||
|
||||
config = await self._get_config_from_file(config_file_path=config_file_path)
|
||||
|
||||
## UPDATE CONFIG WITH DB
|
||||
if prisma_client is not None and store_model_in_db is True:
|
||||
config = await self._update_config_from_db(
|
||||
|
|
@ -1927,6 +1929,7 @@ class ProxyConfig:
|
|||
config = self._check_for_os_environ_vars(config=config)
|
||||
|
||||
self.update_config_state(config=config)
|
||||
|
||||
return config
|
||||
|
||||
def update_config_state(self, config: dict):
|
||||
|
|
|
|||
|
|
@ -3,17 +3,17 @@ from typing import List
|
|||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from litellm.proxy._types import CommonProxyErrors
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
from litellm.proxy.public_endpoints.provider_create_metadata import (
|
||||
get_provider_create_metadata,
|
||||
)
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
from litellm.types.agents import AgentCard
|
||||
from litellm.types.proxy.management_endpoints.model_management_endpoints import (
|
||||
ModelGroupInfoProxy,
|
||||
)
|
||||
from litellm.types.proxy.public_endpoints.public_endpoints import (
|
||||
PublicModelHubInfo,
|
||||
ProviderCreateInfo,
|
||||
PublicModelHubInfo,
|
||||
)
|
||||
from litellm.types.utils import LlmProviders
|
||||
|
||||
|
|
@ -53,10 +53,19 @@ async def public_model_hub():
|
|||
response_model=List[AgentCard],
|
||||
)
|
||||
async def get_agents():
|
||||
import litellm
|
||||
from litellm.proxy.agent_endpoints.agent_registry import global_agent_registry
|
||||
|
||||
agents = global_agent_registry.get_public_agent_list()
|
||||
return [agent.get("agent_card_params") for agent in agents]
|
||||
|
||||
if litellm.public_agent_groups is None:
|
||||
return []
|
||||
agent_card_list = [
|
||||
agent.agent_card_params
|
||||
for agent in agents
|
||||
if agent.agent_id in litellm.public_agent_groups
|
||||
]
|
||||
return agent_card_list
|
||||
|
||||
|
||||
@router.get(
|
||||
|
|
|
|||
|
|
@ -158,14 +158,20 @@ class AgentCard(TypedDict, total=False):
|
|||
signatures: Optional[List[AgentCardSignature]]
|
||||
|
||||
|
||||
class AgentLitellmParams(TypedDict):
|
||||
make_public: bool
|
||||
class AugmentedAgentCard(AgentCard):
|
||||
is_public: bool
|
||||
|
||||
|
||||
class AgentConfig(TypedDict, total=False):
|
||||
agent_name: Required[str]
|
||||
agent_card_params: Required[AgentCard]
|
||||
litellm_params: AgentLitellmParams
|
||||
litellm_params: Dict[str, Any] # allow for any future litellm params
|
||||
|
||||
|
||||
class PatchAgentRequest(TypedDict, total=False):
|
||||
agent_name: str
|
||||
agent_card_params: AgentCard
|
||||
litellm_params: Dict[str, Any]
|
||||
|
||||
|
||||
# Request/Response models for CRUD endpoints
|
||||
|
|
@ -184,3 +190,13 @@ class AgentResponse(BaseModel):
|
|||
|
||||
class ListAgentsResponse(BaseModel):
|
||||
agents: List[AgentResponse]
|
||||
|
||||
|
||||
class AgentMakePublicResponse(BaseModel):
|
||||
message: str
|
||||
public_agent_groups: List[str]
|
||||
updated_by: str
|
||||
|
||||
|
||||
class MakeAgentsPublicRequest(BaseModel):
|
||||
agent_ids: List[str]
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import CacheDashboard from "@/components/cache_dashboard";
|
|||
import { getUiConfig, proxyBaseUrl, setGlobalLitellmHeaderName } from "@/components/networking";
|
||||
import { Organization } from "@/components/networking";
|
||||
import GuardrailsPanel from "@/components/guardrails";
|
||||
import AgentsPanel from "@/components/agents";
|
||||
import PromptsPanel from "@/components/prompts";
|
||||
import TransformRequestPanel from "@/components/transform_request";
|
||||
import { fetchUserModels } from "@/components/organisms/create_key_button";
|
||||
|
|
@ -415,6 +416,8 @@ export default function CreateKeyPage() {
|
|||
<BudgetPanel accessToken={accessToken} />
|
||||
) : page == "guardrails" ? (
|
||||
<GuardrailsPanel accessToken={accessToken} userRole={userRole} />
|
||||
) : page == "agents" ? (
|
||||
<AgentsPanel accessToken={accessToken} userRole={userRole} />
|
||||
) : page == "prompts" ? (
|
||||
<PromptsPanel accessToken={accessToken} userRole={userRole} />
|
||||
) : page == "transform-request" ? (
|
||||
|
|
|
|||
243
ui/litellm-dashboard/src/components/agent_hub_table_columns.tsx
Normal file
243
ui/litellm-dashboard/src/components/agent_hub_table_columns.tsx
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { Button, Badge, Text } from "@tremor/react";
|
||||
import { Tooltip, Tag } from "antd";
|
||||
import { CopyOutlined, InfoCircleOutlined } from "@ant-design/icons";
|
||||
|
||||
export interface AgentHubData {
|
||||
agent_id?: string;
|
||||
protocolVersion: string;
|
||||
name: string;
|
||||
description: string;
|
||||
url: string;
|
||||
version: string;
|
||||
capabilities?: {
|
||||
streaming?: boolean;
|
||||
[key: string]: any;
|
||||
};
|
||||
defaultInputModes?: string[];
|
||||
defaultOutputModes?: string[];
|
||||
skills?: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
tags?: string[];
|
||||
examples?: string[];
|
||||
}>;
|
||||
supportsAuthenticatedExtendedCard?: boolean;
|
||||
is_public?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const agentHubColumns = (
|
||||
showModal: (agent: AgentHubData) => void,
|
||||
copyToClipboard: (text: string) => void,
|
||||
publicPage: boolean = false,
|
||||
): ColumnDef<AgentHubData>[] => {
|
||||
const allColumns: ColumnDef<AgentHubData>[] = [
|
||||
{
|
||||
header: "Agent Name",
|
||||
accessorKey: "name",
|
||||
enableSorting: true,
|
||||
sortingFn: "alphanumeric",
|
||||
cell: ({ row }) => {
|
||||
const agent = row.original;
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Text className="font-medium text-sm">{agent.name}</Text>
|
||||
<Tooltip title="Copy agent name">
|
||||
<CopyOutlined
|
||||
onClick={() => copyToClipboard(agent.name)}
|
||||
className="cursor-pointer text-gray-500 hover:text-blue-500 text-xs"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/* Show description on mobile */}
|
||||
<div className="md:hidden">
|
||||
<Text className="text-xs text-gray-600">{agent.description}</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Description",
|
||||
accessorKey: "description",
|
||||
enableSorting: true,
|
||||
sortingFn: "alphanumeric",
|
||||
cell: ({ row }) => {
|
||||
const agent = row.original;
|
||||
|
||||
return (
|
||||
<Text className="text-xs line-clamp-2">
|
||||
{agent.description || "-"}
|
||||
</Text>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
className: "hidden md:table-cell",
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Version",
|
||||
accessorKey: "version",
|
||||
enableSorting: true,
|
||||
sortingFn: "alphanumeric",
|
||||
cell: ({ row }) => {
|
||||
const agent = row.original;
|
||||
|
||||
return (
|
||||
<Badge color="blue" size="sm">
|
||||
v{agent.version}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
className: "hidden lg:table-cell",
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Protocol",
|
||||
accessorKey: "protocolVersion",
|
||||
enableSorting: true,
|
||||
sortingFn: "alphanumeric",
|
||||
cell: ({ row }) => {
|
||||
const agent = row.original;
|
||||
|
||||
return (
|
||||
<Text className="text-xs">
|
||||
{agent.protocolVersion || "-"}
|
||||
</Text>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
className: "hidden lg:table-cell",
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Skills",
|
||||
accessorKey: "skills",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const agent = row.original;
|
||||
const skills = agent.skills || [];
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<Text className="text-xs font-medium">
|
||||
{skills.length} skill{skills.length !== 1 ? "s" : ""}
|
||||
</Text>
|
||||
{skills.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{skills.slice(0, 2).map((skill) => (
|
||||
<Tag key={skill.id} color="purple" className="text-xs">
|
||||
{skill.name}
|
||||
</Tag>
|
||||
))}
|
||||
{skills.length > 2 && (
|
||||
<Text className="text-xs text-gray-500">+{skills.length - 2}</Text>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Capabilities",
|
||||
accessorKey: "capabilities",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const agent = row.original;
|
||||
const capabilities = agent.capabilities || {};
|
||||
const capabilityList = Object.entries(capabilities)
|
||||
.filter(([_, value]) => value === true)
|
||||
.map(([key]) => key);
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{capabilityList.length === 0 ? (
|
||||
<Text className="text-gray-500 text-xs">-</Text>
|
||||
) : (
|
||||
capabilityList.map((capability) => (
|
||||
<Badge key={capability} color="green" size="xs">
|
||||
{capability}
|
||||
</Badge>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "I/O Modes",
|
||||
accessorKey: "defaultInputModes",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const agent = row.original;
|
||||
const inputModes = agent.defaultInputModes || [];
|
||||
const outputModes = agent.defaultOutputModes || [];
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<Text className="text-xs">
|
||||
<span className="font-medium">In:</span> {inputModes.join(", ") || "-"}
|
||||
</Text>
|
||||
<Text className="text-xs">
|
||||
<span className="font-medium">Out:</span> {outputModes.join(", ") || "-"}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
className: "hidden xl:table-cell",
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Public",
|
||||
accessorKey: "is_public",
|
||||
enableSorting: true,
|
||||
sortingFn: (rowA, rowB) => {
|
||||
const publicA = rowA.original.is_public === true ? 1 : 0;
|
||||
const publicB = rowB.original.is_public === true ? 1 : 0;
|
||||
return publicA - publicB;
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
console.log(`CHECKPOINT 1: ${JSON.stringify(row.original)}`);
|
||||
const agent = row.original;
|
||||
|
||||
return agent.is_public === true ? (
|
||||
<Badge color="green" size="xs">
|
||||
Yes
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge color="gray" size="xs">
|
||||
No
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
className: "hidden md:table-cell",
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Details",
|
||||
id: "details",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const agent = row.original;
|
||||
|
||||
return (
|
||||
<Button size="xs" variant="secondary" onClick={() => showModal(agent)} icon={InfoCircleOutlined}>
|
||||
<span className="hidden lg:inline">Details</span>
|
||||
<span className="lg:hidden">Info</span>
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return allColumns;
|
||||
};
|
||||
|
||||
149
ui/litellm-dashboard/src/components/agents.tsx
Normal file
149
ui/litellm-dashboard/src/components/agents.tsx
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Button } from "@tremor/react";
|
||||
import { Modal } from "antd";
|
||||
import { getAgentsList, deleteAgentCall } from "./networking";
|
||||
import AddAgentForm from "./agents/add_agent_form";
|
||||
import AgentTable from "./agents/agent_table";
|
||||
import { isAdminRole } from "@/utils/roles";
|
||||
import AgentInfoView from "./agents/agent_info";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
import { Agent } from "./agents/types";
|
||||
|
||||
interface AgentsPanelProps {
|
||||
accessToken: string | null;
|
||||
userRole?: string;
|
||||
}
|
||||
|
||||
interface AgentsResponse {
|
||||
agents: Agent[];
|
||||
}
|
||||
|
||||
const AgentsPanel: React.FC<AgentsPanelProps> = ({ accessToken, userRole }) => {
|
||||
const [agentsList, setAgentsList] = useState<Agent[]>([]);
|
||||
const [isAddModalVisible, setIsAddModalVisible] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [agentToDelete, setAgentToDelete] = useState<{ id: string; name: string } | null>(null);
|
||||
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
|
||||
|
||||
const isAdmin = userRole ? isAdminRole(userRole) : false;
|
||||
|
||||
const fetchAgents = async () => {
|
||||
if (!accessToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response: AgentsResponse = await getAgentsList(accessToken);
|
||||
console.log(`agents: ${JSON.stringify(response)}`);
|
||||
setAgentsList(response.agents);
|
||||
} catch (error) {
|
||||
console.error("Error fetching agents:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchAgents();
|
||||
}, [accessToken]);
|
||||
|
||||
const handleAddAgent = () => {
|
||||
if (selectedAgentId) {
|
||||
setSelectedAgentId(null);
|
||||
}
|
||||
setIsAddModalVisible(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setIsAddModalVisible(false);
|
||||
};
|
||||
|
||||
const handleSuccess = () => {
|
||||
fetchAgents();
|
||||
};
|
||||
|
||||
const handleDeleteClick = (agentId: string, agentName: string) => {
|
||||
setAgentToDelete({ id: agentId, name: agentName });
|
||||
};
|
||||
|
||||
const handleDeleteConfirm = async () => {
|
||||
if (!agentToDelete || !accessToken) return;
|
||||
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
await deleteAgentCall(accessToken, agentToDelete.id);
|
||||
NotificationsManager.success(`Agent "${agentToDelete.name}" deleted successfully`);
|
||||
fetchAgents();
|
||||
} catch (error) {
|
||||
console.error("Error deleting agent:", error);
|
||||
NotificationsManager.fromBackend("Failed to delete agent");
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
setAgentToDelete(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteCancel = () => {
|
||||
setAgentToDelete(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full mx-auto flex-auto overflow-y-auto m-8 p-2">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="flex-col gap-2">
|
||||
<h1 className="text-2xl font-bold">Agents</h1>
|
||||
<p className="text-sm text-gray-600">List of A2A-spec agents that are available to be used in your organization. Go to AI Hub, to make agents public.</p>
|
||||
</div>
|
||||
<Button onClick={handleAddAgent} disabled={!accessToken}>
|
||||
+ Add New Agent
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{selectedAgentId ? (
|
||||
<AgentInfoView
|
||||
agentId={selectedAgentId}
|
||||
onClose={() => setSelectedAgentId(null)}
|
||||
accessToken={accessToken}
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
) : (
|
||||
<AgentTable
|
||||
agentsList={agentsList}
|
||||
isLoading={isLoading}
|
||||
onDeleteClick={handleDeleteClick}
|
||||
accessToken={accessToken}
|
||||
onAgentUpdated={fetchAgents}
|
||||
isAdmin={isAdmin}
|
||||
onAgentClick={(id) => setSelectedAgentId(id)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<AddAgentForm
|
||||
visible={isAddModalVisible}
|
||||
onClose={handleCloseModal}
|
||||
accessToken={accessToken}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
|
||||
{agentToDelete && (
|
||||
<Modal
|
||||
title="Delete Agent"
|
||||
open={agentToDelete !== null}
|
||||
onOk={handleDeleteConfirm}
|
||||
onCancel={handleDeleteCancel}
|
||||
confirmLoading={isDeleting}
|
||||
okText="Delete"
|
||||
okButtonProps={{ danger: true }}
|
||||
>
|
||||
<p>Are you sure you want to delete agent: {agentToDelete.name}?</p>
|
||||
<p>This action cannot be undone.</p>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentsPanel;
|
||||
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
import React, { useState } from "react";
|
||||
import { Modal, Form, Button as AntButton, message } from "antd";
|
||||
import { createAgentCall } from "../networking";
|
||||
import AgentFormFields from "./agent_form_fields";
|
||||
import { getDefaultFormValues, buildAgentDataFromForm } from "./agent_config";
|
||||
|
||||
interface AddAgentFormProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
accessToken: string | null;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const AddAgentForm: React.FC<AddAgentFormProps> = ({
|
||||
visible,
|
||||
onClose,
|
||||
accessToken,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const handleSubmit = async (values: any) => {
|
||||
if (!accessToken) {
|
||||
message.error("No access token available");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const agentData = buildAgentDataFromForm(values);
|
||||
await createAgentCall(accessToken, agentData);
|
||||
message.success("Agent created successfully");
|
||||
form.resetFields();
|
||||
onSuccess();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("Error creating agent:", error);
|
||||
message.error("Failed to create agent");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
form.resetFields();
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Add New Agent"
|
||||
open={visible}
|
||||
onCancel={handleCancel}
|
||||
footer={null}
|
||||
width={800}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
initialValues={getDefaultFormValues()}
|
||||
>
|
||||
<AgentFormFields showAgentName={true} />
|
||||
|
||||
<Form.Item>
|
||||
<div style={{ display: "flex", justifyContent: "flex-end", gap: "8px" }}>
|
||||
<AntButton onClick={handleCancel}>
|
||||
Cancel
|
||||
</AntButton>
|
||||
<AntButton
|
||||
htmlType="submit"
|
||||
loading={isSubmitting}
|
||||
>
|
||||
Create Agent
|
||||
</AntButton>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddAgentForm;
|
||||
|
||||
271
ui/litellm-dashboard/src/components/agents/agent_config.ts
Normal file
271
ui/litellm-dashboard/src/components/agents/agent_config.ts
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
/**
|
||||
* Shared configuration for agent form fields
|
||||
* Used across create, view, and update operations
|
||||
*/
|
||||
|
||||
export interface FieldConfig {
|
||||
name: string;
|
||||
label: string;
|
||||
type: "text" | "textarea" | "url" | "switch" | "list";
|
||||
required?: boolean;
|
||||
tooltip?: string;
|
||||
placeholder?: string;
|
||||
defaultValue?: any;
|
||||
rows?: number;
|
||||
validation?: any[];
|
||||
}
|
||||
|
||||
export interface SectionConfig {
|
||||
key: string;
|
||||
title: string;
|
||||
fields: FieldConfig[];
|
||||
defaultExpanded?: boolean;
|
||||
}
|
||||
|
||||
export const AGENT_FORM_CONFIG: {
|
||||
basic: SectionConfig;
|
||||
skills: SectionConfig;
|
||||
capabilities: SectionConfig;
|
||||
optional: SectionConfig;
|
||||
litellm: SectionConfig;
|
||||
} = {
|
||||
basic: {
|
||||
key: "basic",
|
||||
title: "Basic Information",
|
||||
defaultExpanded: true,
|
||||
fields: [
|
||||
{
|
||||
name: "name",
|
||||
label: "Display Name",
|
||||
type: "text",
|
||||
required: true,
|
||||
placeholder: "e.g., Customer Support Agent",
|
||||
},
|
||||
{
|
||||
name: "description",
|
||||
label: "Description",
|
||||
type: "textarea",
|
||||
required: true,
|
||||
placeholder: "Describe what this agent does...",
|
||||
rows: 3,
|
||||
},
|
||||
{
|
||||
name: "url",
|
||||
label: "URL",
|
||||
type: "url",
|
||||
required: true,
|
||||
placeholder: "http://localhost:9999/",
|
||||
tooltip: "Base URL where the agent is hosted",
|
||||
},
|
||||
{
|
||||
name: "version",
|
||||
label: "Version",
|
||||
type: "text",
|
||||
placeholder: "1.0.0",
|
||||
defaultValue: "1.0.0",
|
||||
},
|
||||
{
|
||||
name: "protocolVersion",
|
||||
label: "Protocol Version",
|
||||
type: "text",
|
||||
placeholder: "1.0",
|
||||
defaultValue: "1.0",
|
||||
},
|
||||
],
|
||||
},
|
||||
skills: {
|
||||
key: "skills",
|
||||
title: "Skills",
|
||||
fields: [
|
||||
{
|
||||
name: "skills",
|
||||
label: "Skills",
|
||||
type: "list",
|
||||
defaultValue: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
capabilities: {
|
||||
key: "capabilities",
|
||||
title: "Capabilities",
|
||||
fields: [
|
||||
{
|
||||
name: "streaming",
|
||||
label: "Streaming",
|
||||
type: "switch",
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
name: "pushNotifications",
|
||||
label: "Push Notifications",
|
||||
type: "switch",
|
||||
},
|
||||
{
|
||||
name: "stateTransitionHistory",
|
||||
label: "State Transition History",
|
||||
type: "switch",
|
||||
},
|
||||
],
|
||||
},
|
||||
optional: {
|
||||
key: "optional",
|
||||
title: "Optional Settings",
|
||||
fields: [
|
||||
{
|
||||
name: "iconUrl",
|
||||
label: "Icon URL",
|
||||
type: "url",
|
||||
placeholder: "https://example.com/icon.png",
|
||||
},
|
||||
{
|
||||
name: "documentationUrl",
|
||||
label: "Documentation URL",
|
||||
type: "url",
|
||||
placeholder: "https://docs.example.com",
|
||||
},
|
||||
{
|
||||
name: "supportsAuthenticatedExtendedCard",
|
||||
label: "Supports Authenticated Extended Card",
|
||||
type: "switch",
|
||||
},
|
||||
],
|
||||
},
|
||||
litellm: {
|
||||
key: "litellm",
|
||||
title: "LiteLLM Parameters",
|
||||
fields: [
|
||||
{
|
||||
name: "model",
|
||||
label: "Model (Optional)",
|
||||
type: "text",
|
||||
},
|
||||
{
|
||||
name: "make_public",
|
||||
label: "Make Public",
|
||||
type: "switch",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const SKILL_FIELD_CONFIG = {
|
||||
id: {
|
||||
name: "id",
|
||||
label: "Skill ID",
|
||||
required: true,
|
||||
placeholder: "e.g., hello_world",
|
||||
},
|
||||
name: {
|
||||
name: "name",
|
||||
label: "Skill Name",
|
||||
required: true,
|
||||
placeholder: "e.g., Returns hello world",
|
||||
},
|
||||
description: {
|
||||
name: "description",
|
||||
label: "Description",
|
||||
required: true,
|
||||
placeholder: "What this skill does",
|
||||
rows: 2,
|
||||
},
|
||||
tags: {
|
||||
name: "tags",
|
||||
label: "Tags (comma-separated)",
|
||||
required: true,
|
||||
placeholder: "e.g., hello world, greeting",
|
||||
},
|
||||
examples: {
|
||||
name: "examples",
|
||||
label: "Examples (comma-separated)",
|
||||
placeholder: "e.g., hi, hello world",
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Get default form values from configuration
|
||||
*/
|
||||
export const getDefaultFormValues = () => {
|
||||
const defaults: any = {
|
||||
defaultInputModes: ["text"],
|
||||
defaultOutputModes: ["text"],
|
||||
};
|
||||
|
||||
Object.values(AGENT_FORM_CONFIG).forEach((section) => {
|
||||
section.fields.forEach((field) => {
|
||||
if (field.defaultValue !== undefined) {
|
||||
defaults[field.name] = field.defaultValue;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return defaults;
|
||||
};
|
||||
|
||||
/**
|
||||
* Build agent data from form values according to AgentConfig spec
|
||||
*/
|
||||
export const buildAgentDataFromForm = (values: any, existingAgent?: any) => {
|
||||
const agentData: any = {
|
||||
agent_name: values.agent_name,
|
||||
agent_card_params: {
|
||||
protocolVersion: values.protocolVersion || "1.0",
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
url: values.url,
|
||||
version: values.version || "1.0.0",
|
||||
defaultInputModes: existingAgent?.agent_card_params?.defaultInputModes || ["text"],
|
||||
defaultOutputModes: existingAgent?.agent_card_params?.defaultOutputModes || ["text"],
|
||||
capabilities: {
|
||||
streaming: values.streaming === true,
|
||||
...(values.pushNotifications !== undefined && { pushNotifications: values.pushNotifications }),
|
||||
...(values.stateTransitionHistory !== undefined && { stateTransitionHistory: values.stateTransitionHistory }),
|
||||
},
|
||||
skills: values.skills || [],
|
||||
...(values.iconUrl && { iconUrl: values.iconUrl }),
|
||||
...(values.documentationUrl && { documentationUrl: values.documentationUrl }),
|
||||
...(values.supportsAuthenticatedExtendedCard !== undefined && {
|
||||
supportsAuthenticatedExtendedCard: values.supportsAuthenticatedExtendedCard,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
// Only add litellm_params if there are values
|
||||
if (values.model || values.make_public !== undefined) {
|
||||
agentData.litellm_params = {
|
||||
...(values.model && { model: values.model }),
|
||||
...(values.make_public !== undefined && { make_public: values.make_public }),
|
||||
};
|
||||
}
|
||||
|
||||
return agentData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse agent data for form fields
|
||||
*/
|
||||
export const parseAgentForForm = (agent: any) => {
|
||||
const skills =
|
||||
agent.agent_card_params?.skills?.map((skill: any) => ({
|
||||
...skill,
|
||||
tags: skill.tags,
|
||||
examples: skill.examples || [],
|
||||
})) || [];
|
||||
|
||||
return {
|
||||
agent_name: agent.agent_name,
|
||||
name: agent.agent_card_params?.name,
|
||||
description: agent.agent_card_params?.description,
|
||||
url: agent.agent_card_params?.url,
|
||||
version: agent.agent_card_params?.version,
|
||||
protocolVersion: agent.agent_card_params?.protocolVersion,
|
||||
streaming: agent.agent_card_params?.capabilities?.streaming,
|
||||
pushNotifications: agent.agent_card_params?.capabilities?.pushNotifications,
|
||||
stateTransitionHistory: agent.agent_card_params?.capabilities?.stateTransitionHistory,
|
||||
skills: skills,
|
||||
iconUrl: agent.agent_card_params?.iconUrl,
|
||||
documentationUrl: agent.agent_card_params?.documentationUrl,
|
||||
supportsAuthenticatedExtendedCard: agent.agent_card_params?.supportsAuthenticatedExtendedCard,
|
||||
model: agent.litellm_params?.model,
|
||||
make_public: agent.litellm_params?.make_public,
|
||||
};
|
||||
};
|
||||
176
ui/litellm-dashboard/src/components/agents/agent_form_fields.tsx
Normal file
176
ui/litellm-dashboard/src/components/agents/agent_form_fields.tsx
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
import React from "react";
|
||||
import { Form, Input, Switch, Collapse } from "antd";
|
||||
import { Button as AntButton } from "antd";
|
||||
import { PlusOutlined, MinusCircleOutlined } from "@ant-design/icons";
|
||||
import { AGENT_FORM_CONFIG, SKILL_FIELD_CONFIG } from "./agent_config";
|
||||
|
||||
const { Panel } = Collapse;
|
||||
|
||||
interface AgentFormFieldsProps {
|
||||
showAgentName?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable form fields component for agent forms
|
||||
* Uses shared configuration from agent_config.ts
|
||||
*/
|
||||
const AgentFormFields: React.FC<AgentFormFieldsProps> = ({ showAgentName = true }) => {
|
||||
return (
|
||||
<>
|
||||
{showAgentName && (
|
||||
<Form.Item
|
||||
label="Agent Name"
|
||||
name="agent_name"
|
||||
rules={[{ required: true, message: "Please enter a unique agent name" }]}
|
||||
tooltip="Unique identifier for the agent"
|
||||
>
|
||||
<Input placeholder="e.g., customer-support-agent" />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Collapse defaultActiveKey={['basic']} style={{ marginBottom: 16 }}>
|
||||
{/* Basic Information */}
|
||||
<Panel header={`${AGENT_FORM_CONFIG.basic.title} (Required)`} key={AGENT_FORM_CONFIG.basic.key}>
|
||||
{AGENT_FORM_CONFIG.basic.fields.map((field) => (
|
||||
<Form.Item
|
||||
key={field.name}
|
||||
label={field.label}
|
||||
name={field.name}
|
||||
rules={field.required ? [{ required: true, message: `Please enter ${field.label.toLowerCase()}` }] : undefined}
|
||||
tooltip={field.tooltip}
|
||||
>
|
||||
{field.type === 'textarea' ? (
|
||||
<Input.TextArea rows={field.rows} placeholder={field.placeholder} />
|
||||
) : (
|
||||
<Input placeholder={field.placeholder} />
|
||||
)}
|
||||
</Form.Item>
|
||||
))}
|
||||
</Panel>
|
||||
|
||||
{/* Skills */}
|
||||
<Panel header={`${AGENT_FORM_CONFIG.skills.title} (Required)`} key={AGENT_FORM_CONFIG.skills.key}>
|
||||
<Form.List name="skills">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map((field) => (
|
||||
<div key={field.key} style={{ marginBottom: 16, padding: 16, border: '1px solid #d9d9d9', borderRadius: 4 }}>
|
||||
<Form.Item
|
||||
{...field}
|
||||
label={SKILL_FIELD_CONFIG.id.label}
|
||||
name={[field.name, 'id']}
|
||||
rules={[{ required: SKILL_FIELD_CONFIG.id.required, message: 'Required' }]}
|
||||
>
|
||||
<Input placeholder={SKILL_FIELD_CONFIG.id.placeholder} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
{...field}
|
||||
label={SKILL_FIELD_CONFIG.name.label}
|
||||
name={[field.name, 'name']}
|
||||
rules={[{ required: SKILL_FIELD_CONFIG.name.required, message: 'Required' }]}
|
||||
>
|
||||
<Input placeholder={SKILL_FIELD_CONFIG.name.placeholder} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
{...field}
|
||||
label={SKILL_FIELD_CONFIG.description.label}
|
||||
name={[field.name, 'description']}
|
||||
rules={[{ required: SKILL_FIELD_CONFIG.description.required, message: 'Required' }]}
|
||||
>
|
||||
<Input.TextArea rows={SKILL_FIELD_CONFIG.description.rows} placeholder={SKILL_FIELD_CONFIG.description.placeholder} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
{...field}
|
||||
label={SKILL_FIELD_CONFIG.tags.label}
|
||||
name={[field.name, 'tags']}
|
||||
rules={[{ required: SKILL_FIELD_CONFIG.tags.required, message: 'Required' }]}
|
||||
getValueFromEvent={(e) => e.target.value.split(',').map((s: string) => s.trim())}
|
||||
getValueProps={(value) => ({ value: Array.isArray(value) ? value.join(', ') : value })}
|
||||
>
|
||||
<Input placeholder={SKILL_FIELD_CONFIG.tags.placeholder} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
{...field}
|
||||
label={SKILL_FIELD_CONFIG.examples.label}
|
||||
name={[field.name, 'examples']}
|
||||
getValueFromEvent={(e) => e.target.value.split(',').map((s: string) => s.trim()).filter((s: string) => s)}
|
||||
getValueProps={(value) => ({ value: Array.isArray(value) ? value.join(', ') : '' })}
|
||||
>
|
||||
<Input placeholder={SKILL_FIELD_CONFIG.examples.placeholder} />
|
||||
</Form.Item>
|
||||
|
||||
<AntButton
|
||||
type="link"
|
||||
danger
|
||||
onClick={() => remove(field.name)}
|
||||
icon={<MinusCircleOutlined />}
|
||||
>
|
||||
Remove Skill
|
||||
</AntButton>
|
||||
</div>
|
||||
))}
|
||||
<AntButton
|
||||
type="dashed"
|
||||
onClick={() => add()}
|
||||
icon={<PlusOutlined />}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
Add Skill
|
||||
</AntButton>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</Panel>
|
||||
|
||||
{/* Capabilities */}
|
||||
<Panel header={AGENT_FORM_CONFIG.capabilities.title} key={AGENT_FORM_CONFIG.capabilities.key}>
|
||||
{AGENT_FORM_CONFIG.capabilities.fields.map((field) => (
|
||||
<Form.Item
|
||||
key={field.name}
|
||||
label={field.label}
|
||||
name={field.name}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
))}
|
||||
</Panel>
|
||||
|
||||
{/* Optional Settings */}
|
||||
<Panel header={AGENT_FORM_CONFIG.optional.title} key={AGENT_FORM_CONFIG.optional.key}>
|
||||
{AGENT_FORM_CONFIG.optional.fields.map((field) => (
|
||||
<Form.Item
|
||||
key={field.name}
|
||||
label={field.label}
|
||||
name={field.name}
|
||||
valuePropName={field.type === 'switch' ? 'checked' : undefined}
|
||||
>
|
||||
{field.type === 'switch' ? <Switch /> : <Input placeholder={field.placeholder} />}
|
||||
</Form.Item>
|
||||
))}
|
||||
</Panel>
|
||||
|
||||
{/* LiteLLM Parameters */}
|
||||
<Panel header={AGENT_FORM_CONFIG.litellm.title} key={AGENT_FORM_CONFIG.litellm.key}>
|
||||
{AGENT_FORM_CONFIG.litellm.fields.map((field) => (
|
||||
<Form.Item
|
||||
key={field.name}
|
||||
label={field.label}
|
||||
name={field.name}
|
||||
valuePropName={field.type === 'switch' ? 'checked' : undefined}
|
||||
>
|
||||
{field.type === 'switch' ? <Switch /> : <Input placeholder={field.placeholder} />}
|
||||
</Form.Item>
|
||||
))}
|
||||
</Panel>
|
||||
</Collapse>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentFormFields;
|
||||
|
||||
219
ui/litellm-dashboard/src/components/agents/agent_info.tsx
Normal file
219
ui/litellm-dashboard/src/components/agents/agent_info.tsx
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Card, Title, Text, Button as TremorButton, Tab, TabGroup, TabList, TabPanel, TabPanels} from "@tremor/react";
|
||||
import { Form, Input, Button as AntButton, message, Spin, Descriptions } from "antd";
|
||||
import { ArrowLeftIcon } from "@heroicons/react/outline";
|
||||
import { getAgentInfo, patchAgentCall } from "../networking";
|
||||
import { Agent } from "./types";
|
||||
import AgentFormFields from "./agent_form_fields";
|
||||
import { buildAgentDataFromForm, parseAgentForForm } from "./agent_config";
|
||||
|
||||
interface AgentInfoViewProps {
|
||||
agentId: string;
|
||||
onClose: () => void;
|
||||
accessToken: string | null;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
const AgentInfoView: React.FC<AgentInfoViewProps> = ({
|
||||
agentId,
|
||||
onClose,
|
||||
accessToken,
|
||||
isAdmin,
|
||||
}) => {
|
||||
const [agent, setAgent] = useState<Agent | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
fetchAgentInfo();
|
||||
}, [agentId, accessToken]);
|
||||
|
||||
const fetchAgentInfo = async () => {
|
||||
if (!accessToken) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const data = await getAgentInfo(accessToken, agentId);
|
||||
setAgent(data);
|
||||
form.setFieldsValue(parseAgentForForm(data));
|
||||
} catch (error) {
|
||||
console.error("Error fetching agent info:", error);
|
||||
message.error("Failed to load agent information");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdate = async (values: any) => {
|
||||
if (!accessToken || !agent) return;
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const updateData = buildAgentDataFromForm(values, agent);
|
||||
await patchAgentCall(accessToken, agentId, updateData);
|
||||
message.success("Agent updated successfully");
|
||||
setIsEditing(false);
|
||||
fetchAgentInfo();
|
||||
} catch (error) {
|
||||
console.error("Error updating agent:", error);
|
||||
message.error("Failed to update agent");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!agent) {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="text-center">Agent not found</div>
|
||||
<TremorButton onClick={onClose} className="mt-4">
|
||||
Back to Agents List
|
||||
</TremorButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Format date helper function
|
||||
const formatDate = (dateString?: string) => {
|
||||
if (!dateString) return "-";
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div>
|
||||
<TremorButton icon={ArrowLeftIcon} variant="light" onClick={onClose} className="mb-4">
|
||||
Back to Agents
|
||||
</TremorButton>
|
||||
<Title>{agent.agent_name || "Unnamed Agent"}</Title>
|
||||
<Text className="text-gray-500 font-mono">{agent.agent_id}</Text>
|
||||
</div>
|
||||
|
||||
<TabGroup>
|
||||
<TabList className="mb-4">
|
||||
<Tab key="overview">Overview</Tab>
|
||||
{isAdmin ? <Tab key="settings">Settings</Tab> : <></>}
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
{/* Overview Panel */}
|
||||
<TabPanel>
|
||||
<Descriptions bordered column={1}>
|
||||
<Descriptions.Item label="Agent ID">{agent.agent_id}</Descriptions.Item>
|
||||
<Descriptions.Item label="Agent Name">{agent.agent_name}</Descriptions.Item>
|
||||
<Descriptions.Item label="Display Name">{agent.agent_card_params?.name || "-"}</Descriptions.Item>
|
||||
<Descriptions.Item label="Description">{agent.agent_card_params?.description || "-"}</Descriptions.Item>
|
||||
<Descriptions.Item label="URL">{agent.agent_card_params?.url || "-"}</Descriptions.Item>
|
||||
<Descriptions.Item label="Version">{agent.agent_card_params?.version || "-"}</Descriptions.Item>
|
||||
<Descriptions.Item label="Protocol Version">{agent.agent_card_params?.protocolVersion || "-"}</Descriptions.Item>
|
||||
<Descriptions.Item label="Streaming">
|
||||
{agent.agent_card_params?.capabilities?.streaming ? "Yes" : "No"}
|
||||
</Descriptions.Item>
|
||||
{agent.agent_card_params?.capabilities?.pushNotifications && (
|
||||
<Descriptions.Item label="Push Notifications">Yes</Descriptions.Item>
|
||||
)}
|
||||
{agent.agent_card_params?.capabilities?.stateTransitionHistory && (
|
||||
<Descriptions.Item label="State Transition History">Yes</Descriptions.Item>
|
||||
)}
|
||||
<Descriptions.Item label="Skills">
|
||||
{agent.agent_card_params?.skills?.length || 0} configured
|
||||
</Descriptions.Item>
|
||||
{agent.litellm_params?.model && (
|
||||
<Descriptions.Item label="Model">{agent.litellm_params.model}</Descriptions.Item>
|
||||
)}
|
||||
{agent.litellm_params?.make_public !== undefined && (
|
||||
<Descriptions.Item label="Make Public">{agent.litellm_params.make_public ? "Yes" : "No"}</Descriptions.Item>
|
||||
)}
|
||||
{agent.agent_card_params?.iconUrl && (
|
||||
<Descriptions.Item label="Icon URL">{agent.agent_card_params.iconUrl}</Descriptions.Item>
|
||||
)}
|
||||
{agent.agent_card_params?.documentationUrl && (
|
||||
<Descriptions.Item label="Documentation URL">{agent.agent_card_params.documentationUrl}</Descriptions.Item>
|
||||
)}
|
||||
<Descriptions.Item label="Created At">{formatDate(agent.created_at)}</Descriptions.Item>
|
||||
<Descriptions.Item label="Updated At">{formatDate(agent.updated_at)}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
{agent.agent_card_params?.skills && agent.agent_card_params.skills.length > 0 && (
|
||||
<div style={{ marginTop: 24 }}>
|
||||
<Title>Skills</Title>
|
||||
<Descriptions bordered column={1} style={{ marginTop: 16 }}>
|
||||
{agent.agent_card_params.skills.map((skill: any, index: number) => (
|
||||
<Descriptions.Item label={skill.name || `Skill ${index + 1}`} key={index}>
|
||||
<div>
|
||||
<div><strong>ID:</strong> {skill.id}</div>
|
||||
<div><strong>Description:</strong> {skill.description}</div>
|
||||
<div><strong>Tags:</strong> {Array.isArray(skill.tags) ? skill.tags.join(", ") : skill.tags}</div>
|
||||
{skill.examples && skill.examples.length > 0 && (
|
||||
<div><strong>Examples:</strong> {Array.isArray(skill.examples) ? skill.examples.join(", ") : skill.examples}</div>
|
||||
)}
|
||||
</div>
|
||||
</Descriptions.Item>
|
||||
))}
|
||||
</Descriptions>
|
||||
</div>
|
||||
)}
|
||||
</TabPanel>
|
||||
|
||||
{/* Settings Panel (only for admins) */}
|
||||
{isAdmin && (
|
||||
<TabPanel>
|
||||
<Card>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Title>Agent Settings</Title>
|
||||
{!isEditing && (
|
||||
<TremorButton onClick={() => setIsEditing(true)}>Edit Settings</TremorButton>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isEditing ? (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleUpdate}
|
||||
>
|
||||
<Form.Item label="Agent ID">
|
||||
<Input value={agent.agent_id} disabled />
|
||||
</Form.Item>
|
||||
|
||||
<AgentFormFields showAgentName={true} />
|
||||
|
||||
<div className="flex justify-end gap-2 mt-6">
|
||||
<AntButton onClick={() => {
|
||||
setIsEditing(false);
|
||||
fetchAgentInfo();
|
||||
}}>
|
||||
Cancel
|
||||
</AntButton>
|
||||
<TremorButton loading={isSaving}>
|
||||
Save Changes
|
||||
</TremorButton>
|
||||
</div>
|
||||
</Form>
|
||||
) : (
|
||||
<Text>Click "Edit Settings" to modify agent configuration.</Text>
|
||||
)}
|
||||
</Card>
|
||||
</TabPanel>
|
||||
)}
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentInfoView;
|
||||
|
||||
93
ui/litellm-dashboard/src/components/agents/agent_table.tsx
Normal file
93
ui/litellm-dashboard/src/components/agents/agent_table.tsx
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import React from "react";
|
||||
import { Table, TableHead, TableRow, TableHeaderCell, TableBody, TableCell, Button, Icon } from "@tremor/react";
|
||||
import { TrashIcon } from "@heroicons/react/outline";
|
||||
import { Tooltip } from "antd";
|
||||
import { Agent } from "./types";
|
||||
|
||||
interface AgentTableProps {
|
||||
agentsList: Agent[];
|
||||
isLoading: boolean;
|
||||
onDeleteClick: (agentId: string, agentName: string) => void;
|
||||
accessToken: string | null;
|
||||
onAgentUpdated: () => void;
|
||||
isAdmin: boolean;
|
||||
onAgentClick: (agentId: string) => void;
|
||||
}
|
||||
|
||||
const AgentTable: React.FC<AgentTableProps> = ({
|
||||
agentsList,
|
||||
isLoading,
|
||||
onDeleteClick,
|
||||
accessToken,
|
||||
onAgentUpdated,
|
||||
isAdmin,
|
||||
onAgentClick,
|
||||
}) => {
|
||||
if (isLoading) {
|
||||
return <div>Loading agents...</div>;
|
||||
}
|
||||
|
||||
if (!agentsList || agentsList.length === 0) {
|
||||
return <div>No agents found. Create one to get started.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Agent Name</TableHeaderCell>
|
||||
<TableHeaderCell>Description</TableHeaderCell>
|
||||
<TableHeaderCell>Created At</TableHeaderCell>
|
||||
{isAdmin && <TableHeaderCell>Actions</TableHeaderCell>}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{agentsList.map((agent) => (
|
||||
<TableRow key={agent.agent_id}>
|
||||
<TableCell>
|
||||
<Tooltip title={agent.agent_name || ""}>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate max-w-[200px]"
|
||||
onClick={() => onAgentClick(agent.agent_id)}
|
||||
>
|
||||
{agent.agent_name || ""}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{agent.agent_card_params?.description || "No description"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{agent.created_at
|
||||
? new Date(agent.created_at).toLocaleDateString()
|
||||
: "N/A"}
|
||||
</TableCell>
|
||||
{isAdmin && (
|
||||
<TableCell>
|
||||
<div className="flex space-x-2">
|
||||
<Tooltip title="Delete agent">
|
||||
<Icon
|
||||
icon={TrashIcon}
|
||||
size="sm"
|
||||
className="cursor-pointer text-red-500 hover:text-red-700"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDeleteClick(agent.agent_id, agent.agent_name);
|
||||
}}
|
||||
aria-label="Delete agent"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentTable;
|
||||
|
||||
20
ui/litellm-dashboard/src/components/agents/types.ts
Normal file
20
ui/litellm-dashboard/src/components/agents/types.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
export interface Agent {
|
||||
agent_id: string;
|
||||
agent_name: string;
|
||||
litellm_params: {
|
||||
model: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
agent_card_params?: {
|
||||
description?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
created_by?: string;
|
||||
updated_by?: string;
|
||||
}
|
||||
|
||||
export interface AgentsResponse {
|
||||
agents: Agent[];
|
||||
}
|
||||
|
|
@ -41,9 +41,10 @@ describe("Sidebar (leftnav)", () => {
|
|||
"Internal Users",
|
||||
"Budgets",
|
||||
"API Reference",
|
||||
"Model Hub",
|
||||
"AI Hub",
|
||||
"Logs",
|
||||
"Guardrails",
|
||||
"MCP Servers",
|
||||
"Tools",
|
||||
"Experimental",
|
||||
"Settings",
|
||||
|
|
@ -54,15 +55,15 @@ describe("Sidebar (leftnav)", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("expands a nested tab to reveal its children (Tools > MCP Servers)", async () => {
|
||||
it("expands a nested tab to reveal its children (Tools > Search Tools)", async () => {
|
||||
const { getByText, queryByText } = render(<Sidebar {...defaultProps} />);
|
||||
|
||||
expect(queryByText("MCP Servers")).not.toBeInTheDocument();
|
||||
expect(queryByText("Search Tools")).not.toBeInTheDocument();
|
||||
act(() => {
|
||||
fireEvent.click(getByText("Tools"));
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(getByText("MCP Servers")).toBeInTheDocument();
|
||||
expect(getByText("Search Tools")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
KeyOutlined,
|
||||
LineChartOutlined,
|
||||
PlayCircleOutlined,
|
||||
RobotOutlined,
|
||||
SafetyOutlined,
|
||||
SearchOutlined,
|
||||
SettingOutlined,
|
||||
|
|
@ -100,7 +101,7 @@ const Sidebar: React.FC<SidebarProps> = ({ accessToken, setPage, userRole, defau
|
|||
{
|
||||
key: "16",
|
||||
page: "model-hub-table",
|
||||
label: "Model Hub",
|
||||
label: "AI Hub",
|
||||
icon: <AppstoreOutlined style={{ fontSize: "18px" }} />,
|
||||
},
|
||||
{ key: "15", page: "logs", label: "Logs", icon: <LineChartOutlined style={{ fontSize: "18px" }} /> },
|
||||
|
|
@ -111,13 +112,13 @@ const Sidebar: React.FC<SidebarProps> = ({ accessToken, setPage, userRole, defau
|
|||
icon: <SafetyOutlined style={{ fontSize: "18px" }} />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{ key: "18", page: "mcp-servers", label: "MCP Servers", icon: <ToolOutlined style={{ fontSize: "18px" }} /> },
|
||||
{
|
||||
key: "26",
|
||||
page: "tools",
|
||||
label: "Tools",
|
||||
icon: <ToolOutlined style={{ fontSize: "18px" }} />,
|
||||
children: [
|
||||
{ key: "18", page: "mcp-servers", label: "MCP Servers", icon: <ToolOutlined style={{ fontSize: "18px" }} /> },
|
||||
{
|
||||
key: "28",
|
||||
page: "search-tools",
|
||||
|
|
@ -146,6 +147,13 @@ const Sidebar: React.FC<SidebarProps> = ({ accessToken, setPage, userRole, defau
|
|||
icon: <DatabaseOutlined style={{ fontSize: "18px" }} />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
"key": "29",
|
||||
"page": "agents",
|
||||
"label": "Agents",
|
||||
"icon": <RobotOutlined style={{ fontSize: "18px" }} />,
|
||||
"roles": rolesWithWriteAccess,
|
||||
},
|
||||
{
|
||||
key: "25",
|
||||
page: "prompts",
|
||||
|
|
|
|||
299
ui/litellm-dashboard/src/components/make_agent_public_form.tsx
Normal file
299
ui/litellm-dashboard/src/components/make_agent_public_form.tsx
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Modal, Form, Steps, Button, Checkbox } from "antd";
|
||||
import { Text, Title, Badge } from "@tremor/react";
|
||||
import { makeAgentsPublicCall } from "./networking";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
import { AgentHubData } from "./agent_hub_table_columns";
|
||||
|
||||
const { Step } = Steps;
|
||||
|
||||
interface MakeAgentPublicFormProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
accessToken: string;
|
||||
agentHubData: AgentHubData[];
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const MakeAgentPublicForm: React.FC<MakeAgentPublicFormProps> = ({
|
||||
visible,
|
||||
onClose,
|
||||
accessToken,
|
||||
agentHubData,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [selectedAgents, setSelectedAgents] = useState<Set<string>>(new Set());
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const handleClose = () => {
|
||||
setCurrentStep(0);
|
||||
setSelectedAgents(new Set());
|
||||
form.resetFields();
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentStep === 0) {
|
||||
if (selectedAgents.size === 0) {
|
||||
NotificationsManager.fromBackend("Please select at least one agent to make public");
|
||||
return;
|
||||
}
|
||||
setCurrentStep(1);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrevious = () => {
|
||||
if (currentStep === 1) {
|
||||
setCurrentStep(0);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAgentSelection = (agentId: string, checked: boolean) => {
|
||||
const newSelection = new Set(selectedAgents);
|
||||
if (checked) {
|
||||
newSelection.add(agentId);
|
||||
} else {
|
||||
newSelection.delete(agentId);
|
||||
}
|
||||
setSelectedAgents(newSelection);
|
||||
};
|
||||
|
||||
const handleSelectAll = (checked: boolean) => {
|
||||
if (checked) {
|
||||
const allAgentIds = agentHubData.map((agent) => agent.agent_id || agent.name);
|
||||
setSelectedAgents(new Set(allAgentIds));
|
||||
} else {
|
||||
setSelectedAgents(new Set());
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize and preselect already public agents when modal opens
|
||||
useEffect(() => {
|
||||
if (visible && agentHubData.length > 0) {
|
||||
// Preselect agents that are already public
|
||||
const alreadyPublicAgents = agentHubData
|
||||
.filter((agent) => agent.is_public === true)
|
||||
.map((agent) => agent.agent_id || agent.name);
|
||||
|
||||
setSelectedAgents(new Set(alreadyPublicAgents));
|
||||
}
|
||||
}, [visible, agentHubData]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (selectedAgents.size === 0) {
|
||||
NotificationsManager.fromBackend("Please select at least one agent to make public");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const agentIdsToMakePublic = Array.from(selectedAgents);
|
||||
|
||||
// Make batch API call for all agents
|
||||
await makeAgentsPublicCall(accessToken, agentIdsToMakePublic);
|
||||
|
||||
NotificationsManager.success(`Successfully made ${agentIdsToMakePublic.length} agent(s) public!`);
|
||||
handleClose();
|
||||
onSuccess();
|
||||
} catch (error) {
|
||||
console.error("Error making agents public:", error);
|
||||
NotificationsManager.fromBackend("Failed to make agents public. Please try again.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderStep1Content = () => {
|
||||
const allAgentsSelected =
|
||||
agentHubData.length > 0 && agentHubData.every((agent) => selectedAgents.has(agent.agent_id || agent.name));
|
||||
const isIndeterminate = selectedAgents.size > 0 && !allAgentsSelected;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Title>Select Agents to Make Public</Title>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={allAgentsSelected}
|
||||
indeterminate={isIndeterminate}
|
||||
onChange={(e) => handleSelectAll(e.target.checked)}
|
||||
disabled={agentHubData.length === 0}
|
||||
>
|
||||
Select All {agentHubData.length > 0 && `(${agentHubData.length})`}
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Text className="text-sm text-gray-600">
|
||||
Select the agents you want to be visible on the public model hub. Users will still require a valid API key to
|
||||
use these agents.
|
||||
</Text>
|
||||
|
||||
<div className="max-h-96 overflow-y-auto border rounded-lg p-4">
|
||||
<div className="space-y-3">
|
||||
{agentHubData.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<Text>No agents available.</Text>
|
||||
</div>
|
||||
) : (
|
||||
agentHubData.map((agent) => {
|
||||
const agentId = agent.agent_id || agent.name;
|
||||
return (
|
||||
<div
|
||||
key={agentId}
|
||||
className="flex items-center space-x-3 p-3 border rounded-lg hover:bg-gray-50"
|
||||
>
|
||||
<Checkbox
|
||||
checked={selectedAgents.has(agentId)}
|
||||
onChange={(e) => handleAgentSelection(agentId, e.target.checked)}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Text className="font-medium">{agent.name}</Text>
|
||||
<Badge color="blue" size="sm">
|
||||
v{agent.version}
|
||||
</Badge>
|
||||
</div>
|
||||
<Text className="text-xs text-gray-600 mt-1">{agent.description}</Text>
|
||||
{agent.skills && agent.skills.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{agent.skills.slice(0, 3).map((skill) => (
|
||||
<Badge key={skill.id} color="purple" size="xs">
|
||||
{skill.name}
|
||||
</Badge>
|
||||
))}
|
||||
{agent.skills.length > 3 && (
|
||||
<Text className="text-xs text-gray-500">+{agent.skills.length - 3} more</Text>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedAgents.size > 0 && (
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<Text className="text-sm text-blue-800">
|
||||
<strong>{selectedAgents.size}</strong> agent{selectedAgents.size !== 1 ? "s" : ""} selected
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderStep2Content = () => {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Title>Confirm Making Agents Public</Title>
|
||||
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
||||
<Text className="text-sm text-yellow-800">
|
||||
<strong>Warning:</strong> Once you make these agents public, anyone who can go to the{" "}
|
||||
<code>/ui/model_hub_table</code> will be able to know they exist on the proxy.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Text className="font-medium">Agents to be made public:</Text>
|
||||
<div className="max-h-48 overflow-y-auto border rounded-lg p-3">
|
||||
<div className="space-y-2">
|
||||
{Array.from(selectedAgents).map((agentId) => {
|
||||
const agent = agentHubData.find((a) => (a.agent_id || a.name) === agentId);
|
||||
return (
|
||||
<div key={agentId} className="flex items-center justify-between p-2 bg-gray-50 rounded">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Text className="font-medium">{agent?.name || agentId}</Text>
|
||||
{agent && (
|
||||
<Badge color="blue" size="xs">
|
||||
v{agent.version}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{agent?.description && (
|
||||
<Text className="text-xs text-gray-600 mt-1">{agent.description}</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<Text className="text-sm text-blue-800">
|
||||
Total: <strong>{selectedAgents.size}</strong> agent{selectedAgents.size !== 1 ? "s" : ""} will be made
|
||||
public
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderStepContent = () => {
|
||||
switch (currentStep) {
|
||||
case 0:
|
||||
return renderStep1Content();
|
||||
case 1:
|
||||
return renderStep2Content();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const renderStepButtons = () => {
|
||||
return (
|
||||
<div className="flex justify-between mt-6">
|
||||
<Button onClick={currentStep === 0 ? handleClose : handlePrevious}>
|
||||
{currentStep === 0 ? "Cancel" : "Previous"}
|
||||
</Button>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
{currentStep === 0 && (
|
||||
<Button onClick={handleNext} disabled={selectedAgents.size === 0}>
|
||||
Next
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{currentStep === 1 && (
|
||||
<Button onClick={handleSubmit} loading={loading}>
|
||||
Make Public
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Make Agents Public"
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
footer={null}
|
||||
width={1200}
|
||||
maskClosable={false}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Steps current={currentStep} className="mb-6">
|
||||
<Step title="Select Agents" />
|
||||
<Step title="Confirm" />
|
||||
</Steps>
|
||||
|
||||
{renderStepContent()}
|
||||
{renderStepButtons()}
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default MakeAgentPublicForm;
|
||||
|
||||
|
|
@ -1,15 +1,18 @@
|
|||
import React, { useEffect, useState, useRef, useCallback } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { modelHubCall, modelHubPublicModelsCall, getProxyBaseUrl } from "./networking";
|
||||
import { modelHubCall, modelHubPublicModelsCall, getAgentsList, getProxyBaseUrl } from "./networking";
|
||||
import { getConfigFieldSetting } from "./networking";
|
||||
import { ModelDataTable } from "./model_dashboard/table";
|
||||
import { modelHubColumns } from "./model_hub_table_columns";
|
||||
import { agentHubColumns, AgentHubData } from "./agent_hub_table_columns";
|
||||
import PublicModelHub from "./public_model_hub";
|
||||
import MakeModelPublicForm from "./make_model_public_form";
|
||||
import MakeAgentPublicForm from "./make_agent_public_form";
|
||||
import ModelFilters from "./model_filters";
|
||||
import UsefulLinksManagement from "./useful_links_management";
|
||||
import { Card, Text, Title, Button, Badge } from "@tremor/react";
|
||||
import { Card, Text, Title, Button, Badge, TabGroup, TabList, Tab, TabPanels, TabPanel } from "@tremor/react";
|
||||
import { Modal } from "antd";
|
||||
import { CopyOutlined } from "@ant-design/icons";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { Table as TableInstance } from "@tanstack/react-table";
|
||||
import { Copy } from "lucide-react";
|
||||
|
|
@ -51,8 +54,15 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
|
|||
const [selectedModel, setSelectedModel] = useState<null | ModelGroupInfo>(null);
|
||||
const [filteredData, setFilteredData] = useState<ModelGroupInfo[]>([]);
|
||||
const [isMakePublicModalVisible, setIsMakePublicModalVisible] = useState(false);
|
||||
// Agent Hub state
|
||||
const [agentHubData, setAgentHubData] = useState<AgentHubData[] | null>(null);
|
||||
const [isMakeAgentPublicModalVisible, setIsMakeAgentPublicModalVisible] = useState(false);
|
||||
const [agentLoading, setAgentLoading] = useState<boolean>(true);
|
||||
const [selectedAgent, setSelectedAgent] = useState<null | AgentHubData>(null);
|
||||
const [isAgentModalVisible, setIsAgentModalVisible] = useState(false);
|
||||
const router = useRouter();
|
||||
const tableRef = useRef<TableInstance<any>>(null);
|
||||
const agentTableRef = useRef<TableInstance<any>>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async (accessToken: string) => {
|
||||
|
|
@ -103,11 +113,46 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
|
|||
}
|
||||
}, [accessToken, publicPage]);
|
||||
|
||||
// Fetch Agent Hub data
|
||||
useEffect(() => {
|
||||
const fetchAgentData = async () => {
|
||||
if (!accessToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setAgentLoading(true);
|
||||
const response = await getAgentsList(accessToken);
|
||||
console.log("AgentHubData:", response);
|
||||
let agents = response.agents;
|
||||
let agent_card_list = agents.map((agent: any) => ({
|
||||
agent_id: agent.agent_id,
|
||||
...agent.agent_card_params,
|
||||
is_public: agent.litellm_params.is_public,
|
||||
}));
|
||||
setAgentHubData(agent_card_list);
|
||||
} catch (error) {
|
||||
console.error("There was an error fetching the agent data", error);
|
||||
} finally {
|
||||
setAgentLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!publicPage) {
|
||||
fetchAgentData();
|
||||
}
|
||||
}, [publicPage, accessToken]);
|
||||
|
||||
const showModal = (model: ModelGroupInfo) => {
|
||||
setSelectedModel(model);
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const showAgentModal = (agent: AgentHubData) => {
|
||||
setSelectedAgent(agent);
|
||||
setIsAgentModalVisible(true);
|
||||
};
|
||||
|
||||
const goToPublicModelPage = () => {
|
||||
router.replace(`/model_hub_table?key=${accessToken}`);
|
||||
};
|
||||
|
|
@ -121,16 +166,29 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
|
|||
setIsMakePublicModalVisible(true);
|
||||
};
|
||||
|
||||
const handleMakeAgentPublicPage = () => {
|
||||
if (!accessToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the modal for selecting agents to make public
|
||||
setIsMakeAgentPublicModalVisible(true);
|
||||
};
|
||||
|
||||
const handleOk = () => {
|
||||
setIsModalVisible(false);
|
||||
setIsPublicPageModalVisible(false);
|
||||
setSelectedModel(null);
|
||||
setIsAgentModalVisible(false);
|
||||
setSelectedAgent(null);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsModalVisible(false);
|
||||
setIsPublicPageModalVisible(false);
|
||||
setSelectedModel(null);
|
||||
setIsAgentModalVisible(false);
|
||||
setSelectedAgent(null);
|
||||
};
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
|
|
@ -173,6 +231,27 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
|
|||
}
|
||||
};
|
||||
|
||||
const handleMakeAgentPublicSuccess = () => {
|
||||
// Refresh the agent hub data after successful public operation
|
||||
if (accessToken) {
|
||||
const fetchAgentData = async () => {
|
||||
try {
|
||||
const response = await getAgentsList(accessToken);
|
||||
let agents = response.agents;
|
||||
let agent_card_list = agents.map((agent: any) => ({
|
||||
agent_id: agent.agent_id,
|
||||
...agent.agent_card_params,
|
||||
is_public: agent.is_public,
|
||||
}));
|
||||
setAgentHubData(agent_card_list);
|
||||
} catch (error) {
|
||||
console.error("Error refreshing agent data:", error);
|
||||
}
|
||||
};
|
||||
fetchAgentData();
|
||||
}
|
||||
};
|
||||
|
||||
const handleFilteredDataChange = useCallback((newFilteredData: ModelGroupInfo[]) => {
|
||||
setFilteredData(newFilteredData);
|
||||
}, []);
|
||||
|
|
@ -189,12 +268,13 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
|
|||
<div className="w-full mx-4 h-[75vh]">
|
||||
{publicPage == false ? (
|
||||
<div className="w-full m-2 mt-2 p-8">
|
||||
{/* Header with Title, Description and URL */}
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div className="flex flex-col items-start">
|
||||
<Title className="text-center">Model Hub</Title>
|
||||
<Title className="text-center">AI Hub</Title>
|
||||
{isAdminRole(userRole || "") ? (
|
||||
<p className="text-sm text-gray-600">
|
||||
Make models public for developers to know what models are available on the proxy.
|
||||
Make models and agents public for developers to know what's available.
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm text-gray-600">A list of all public model names personally available to you.</p>
|
||||
|
|
@ -212,12 +292,6 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
|
|||
<Copy size={16} className="text-gray-600" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{publicPage == false && isAdminRole(userRole || "") && (
|
||||
<Button className="ml-4" onClick={() => handleMakePublicPage()}>
|
||||
Make Public
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -228,26 +302,77 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Model Filters and Table */}
|
||||
<Card>
|
||||
{/* Filters */}
|
||||
<ModelFilters modelHubData={modelHubData || []} onFilteredDataChange={handleFilteredDataChange} />
|
||||
{/* Tab System for Model Hub and Agent Hub */}
|
||||
<TabGroup>
|
||||
<TabList className="mb-4">
|
||||
<Tab>Model Hub</Tab>
|
||||
<Tab>Agent Hub</Tab>
|
||||
</TabList>
|
||||
|
||||
{/* Model Table */}
|
||||
<ModelDataTable
|
||||
columns={modelHubColumns(showModal, copyToClipboard, publicPage)}
|
||||
data={filteredData}
|
||||
isLoading={loading}
|
||||
table={tableRef}
|
||||
defaultSorting={[{ id: "model_group", desc: false }]}
|
||||
/>
|
||||
</Card>
|
||||
<TabPanels>
|
||||
{/* Model Hub Tab */}
|
||||
<TabPanel>
|
||||
{/* Model Filters and Table */}
|
||||
<Card>
|
||||
{/* Header with Make Public Button */}
|
||||
{publicPage == false && isAdminRole(userRole || "") && (
|
||||
<div className="flex justify-end mb-4">
|
||||
<Button onClick={() => handleMakePublicPage()}>
|
||||
Select Models to Make Public
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Filters */}
|
||||
<ModelFilters modelHubData={modelHubData || []} onFilteredDataChange={handleFilteredDataChange} />
|
||||
|
||||
<div className="mt-4 text-center space-y-2">
|
||||
<Text className="text-sm text-gray-600">
|
||||
Showing {filteredData.length} of {modelHubData?.length || 0} models
|
||||
</Text>
|
||||
</div>
|
||||
{/* Model Table */}
|
||||
<ModelDataTable
|
||||
columns={modelHubColumns(showModal, copyToClipboard, publicPage)}
|
||||
data={filteredData}
|
||||
isLoading={loading}
|
||||
table={tableRef}
|
||||
defaultSorting={[{ id: "model_group", desc: false }]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<div className="mt-4 text-center space-y-2">
|
||||
<Text className="text-sm text-gray-600">
|
||||
Showing {filteredData.length} of {modelHubData?.length || 0} models
|
||||
</Text>
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
||||
{/* Agent Hub Tab */}
|
||||
<TabPanel>
|
||||
<Card>
|
||||
{/* Header with Make Public Button */}
|
||||
{publicPage == false && isAdminRole(userRole || "") && (
|
||||
<div className="flex justify-end mb-4">
|
||||
<Button onClick={() => handleMakeAgentPublicPage()}>
|
||||
Select Agents to Make Public
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Agent Table */}
|
||||
<ModelDataTable
|
||||
columns={agentHubColumns(showAgentModal, copyToClipboard, publicPage)}
|
||||
data={agentHubData || []}
|
||||
isLoading={agentLoading}
|
||||
table={agentTableRef}
|
||||
defaultSorting={[{ id: "name", desc: false }]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<div className="mt-4 text-center space-y-2">
|
||||
<Text className="text-sm text-gray-600">
|
||||
Showing {agentHubData?.length || 0} agent{agentHubData?.length !== 1 ? "s" : ""}
|
||||
</Text>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</div>
|
||||
) : (
|
||||
<Card className="mx-auto max-w-xl mt-10">
|
||||
|
|
@ -429,6 +554,145 @@ print(response.choices[0].message.content)`}
|
|||
)}
|
||||
</Modal>
|
||||
|
||||
{/* Agent Details Modal */}
|
||||
<Modal
|
||||
title={selectedAgent?.name || "Agent Details"}
|
||||
width={1000}
|
||||
visible={isAgentModalVisible}
|
||||
footer={null}
|
||||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
>
|
||||
{selectedAgent && (
|
||||
<div className="space-y-6">
|
||||
{/* Agent Overview */}
|
||||
<div>
|
||||
<Text className="text-lg font-semibold mb-4">Agent Overview</Text>
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<Text className="font-medium">Name:</Text>
|
||||
<Text>{selectedAgent.name}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Version:</Text>
|
||||
<Badge color="blue">v{selectedAgent.version}</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Protocol Version:</Text>
|
||||
<Text>{selectedAgent.protocolVersion}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">URL:</Text>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Text className="truncate">{selectedAgent.url}</Text>
|
||||
<CopyOutlined
|
||||
onClick={() => copyToClipboard(selectedAgent.url)}
|
||||
className="cursor-pointer text-gray-500 hover:text-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Description:</Text>
|
||||
<Text className="mt-1">{selectedAgent.description}</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Capabilities */}
|
||||
{selectedAgent.capabilities && Object.keys(selectedAgent.capabilities).length > 0 && (
|
||||
<div>
|
||||
<Text className="text-lg font-semibold mb-4">Capabilities</Text>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{Object.entries(selectedAgent.capabilities)
|
||||
.filter(([_, value]) => value === true)
|
||||
.map(([key]) => (
|
||||
<Badge key={key} color="green">
|
||||
{key}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Input/Output Modes */}
|
||||
<div>
|
||||
<Text className="text-lg font-semibold mb-4">Input/Output Modes</Text>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Text className="font-medium">Input Modes:</Text>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{selectedAgent.defaultInputModes?.map((mode) => (
|
||||
<Badge key={mode} color="blue">
|
||||
{mode}
|
||||
</Badge>
|
||||
)) || <Text>Not specified</Text>}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Output Modes:</Text>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{selectedAgent.defaultOutputModes?.map((mode) => (
|
||||
<Badge key={mode} color="purple">
|
||||
{mode}
|
||||
</Badge>
|
||||
)) || <Text>Not specified</Text>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Skills */}
|
||||
{selectedAgent.skills && selectedAgent.skills.length > 0 && (
|
||||
<div>
|
||||
<Text className="text-lg font-semibold mb-4">Skills</Text>
|
||||
<div className="space-y-4">
|
||||
{selectedAgent.skills.map((skill) => (
|
||||
<div key={skill.id} className="border border-gray-200 rounded p-4">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div>
|
||||
<Text className="font-medium text-base">{skill.name}</Text>
|
||||
<Text className="text-xs text-gray-500">ID: {skill.id}</Text>
|
||||
</div>
|
||||
{skill.tags && skill.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{skill.tags.map((tag) => (
|
||||
<Badge key={tag} color="purple" size="xs">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Text className="text-sm mb-2">{skill.description}</Text>
|
||||
{skill.examples && skill.examples.length > 0 && (
|
||||
<div>
|
||||
<Text className="text-xs font-medium text-gray-700">Examples:</Text>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{skill.examples.map((example, idx) => (
|
||||
<Badge key={idx} color="gray" size="xs">
|
||||
{example}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Additional Properties */}
|
||||
{selectedAgent.supportsAuthenticatedExtendedCard && (
|
||||
<div>
|
||||
<Text className="text-lg font-semibold mb-4">Additional Features</Text>
|
||||
<Badge color="green">Supports Authenticated Extended Card</Badge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
{/* Make Model Public Form */}
|
||||
<MakeModelPublicForm
|
||||
visible={isMakePublicModalVisible}
|
||||
|
|
@ -437,6 +701,15 @@ print(response.choices[0].message.content)`}
|
|||
modelHubData={modelHubData || []}
|
||||
onSuccess={handleMakePublicSuccess}
|
||||
/>
|
||||
|
||||
{/* Make Agent Public Form */}
|
||||
<MakeAgentPublicForm
|
||||
visible={isMakeAgentPublicModalVisible}
|
||||
onClose={() => setIsMakeAgentPublicModalVisible(false)}
|
||||
accessToken={accessToken || ""}
|
||||
agentHubData={agentHubData || []}
|
||||
onSuccess={handleMakeAgentPublicSuccess}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1903,6 +1903,17 @@ export const modelHubPublicModelsCall = async () => {
|
|||
return response.json();
|
||||
};
|
||||
|
||||
export const agentHubPublicModelsCall = async () => {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/public/agent_hub` : `/public/agent_hub`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const modelHubCall = async (accessToken: string) => {
|
||||
/**
|
||||
* Get all models on proxy
|
||||
|
|
@ -5299,6 +5310,36 @@ export const patchPromptCall = async (accessToken: string, promptId: string, pro
|
|||
}
|
||||
};
|
||||
|
||||
export const createAgentCall = async (accessToken: string, agentData: any) => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/v1/agents` : `/v1/agents`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...agentData,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.text();
|
||||
handleError(errorData);
|
||||
throw new Error(errorData);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("Create agent response:", data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to create agent:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const createGuardrailCall = async (accessToken: string, guardrailData: any) => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/guardrails` : `/guardrails`;
|
||||
|
|
@ -6408,6 +6449,90 @@ export const resetEmailEventSettings = async (accessToken: string) => {
|
|||
export { type UserInfo } from "./view_users/types"; // Re-export UserInfo
|
||||
export { type Team } from "./key_team_helpers/key_list"; // Re-export Team
|
||||
|
||||
export const deleteAgentCall = async (accessToken: string, agentId: string) => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/v1/agents/${agentId}` : `/v1/agents/${agentId}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.text();
|
||||
handleError(errorData);
|
||||
throw new Error(errorData);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("Delete agent response:", data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to delete agent:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const makeAgentPublicCall = async (accessToken: string, agentId: string) => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/v1/agents/${agentId}/make_public` : `/v1/agents/${agentId}/make_public`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.text();
|
||||
handleError(errorData);
|
||||
throw new Error(errorData);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("Make agent public response:", data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to make agent public:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const makeAgentsPublicCall = async (accessToken: string, agentIds: string[]) => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/v1/agents/make_public` : `/v1/agents/make_public`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
agent_ids: agentIds,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.text();
|
||||
handleError(errorData);
|
||||
throw new Error(errorData);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("Make agents public response:", data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to make agents public:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteGuardrailCall = async (accessToken: string, guardrailId: string) => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/guardrails/${guardrailId}` : `/guardrails/${guardrailId}`;
|
||||
|
|
@ -6493,6 +6618,61 @@ export const getGuardrailProviderSpecificParams = async (accessToken: string) =>
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
export const getAgentsList = async (accessToken: string) => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/v1/agents` : `/v1/agents`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.text();
|
||||
handleError(errorData);
|
||||
throw new Error("Failed to get agents list");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("Agents list response:", data);
|
||||
return { agents: data };
|
||||
} catch (error) {
|
||||
console.error("Failed to get agents list:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getAgentInfo = async (accessToken: string, agentId: string) => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/v1/agents/${agentId}` : `/v1/agents/${agentId}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.text();
|
||||
handleError(errorData);
|
||||
throw new Error("Failed to get agent info");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("Agent info response:", data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to get agent info:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getGuardrailInfo = async (accessToken: string, guardrailId: string) => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/guardrails/${guardrailId}/info` : `/guardrails/${guardrailId}/info`;
|
||||
|
|
@ -6520,6 +6700,43 @@ export const getGuardrailInfo = async (accessToken: string, guardrailId: string)
|
|||
}
|
||||
};
|
||||
|
||||
export const patchAgentCall = async (
|
||||
accessToken: string,
|
||||
agentId: string,
|
||||
updateData: {
|
||||
agent_name?: string;
|
||||
litellm_params?: Record<string, any>;
|
||||
agent_card_params?: Record<string, any>;
|
||||
},
|
||||
) => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/v1/agents/${agentId}` : `/v1/agents/${agentId}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(updateData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.text();
|
||||
handleError(errorData);
|
||||
throw new Error("Failed to patch agent");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("Patch agent response:", data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to update guardrail:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const updateGuardrailCall = async (
|
||||
accessToken: string,
|
||||
guardrailId: string,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import React, { useEffect, useState, useRef, useMemo } from "react";
|
||||
import { modelHubPublicModelsCall, getPublicModelHubInfo } from "./networking";
|
||||
import { modelHubPublicModelsCall, getPublicModelHubInfo, agentHubPublicModelsCall } from "./networking";
|
||||
import { ModelDataTable } from "./model_dashboard/table";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { Card, Text, Title, Button } from "@tremor/react";
|
||||
import { Tag, Tooltip, Modal, Select } from "antd";
|
||||
import { Tag, Tooltip, Modal, Select, Tabs } from "antd";
|
||||
import { ExternalLinkIcon, SearchIcon } from "@heroicons/react/outline";
|
||||
import { Copy, Info } from "lucide-react";
|
||||
import { Table as TableInstance } from "@tanstack/react-table";
|
||||
|
|
@ -15,6 +15,8 @@ import Navbar from "./navbar";
|
|||
import { ThemeProvider } from "@/contexts/ThemeContext";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
interface ModelGroupInfo {
|
||||
model_group: string;
|
||||
providers: string[];
|
||||
|
|
@ -32,26 +34,62 @@ interface ModelGroupInfo {
|
|||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface AgentCard {
|
||||
protocolVersion: string;
|
||||
name: string;
|
||||
description: string;
|
||||
url: string;
|
||||
version: string;
|
||||
capabilities?: {
|
||||
streaming?: boolean;
|
||||
pushNotifications?: boolean;
|
||||
stateTransitionHistory?: boolean;
|
||||
};
|
||||
defaultInputModes: string[];
|
||||
defaultOutputModes: string[];
|
||||
skills: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
}>;
|
||||
iconUrl?: string;
|
||||
provider?: {
|
||||
organization: string;
|
||||
url: string;
|
||||
};
|
||||
documentationUrl?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface PublicModelHubProps {
|
||||
accessToken?: string | null;
|
||||
}
|
||||
|
||||
const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken }) => {
|
||||
const [modelHubData, setModelHubData] = useState<ModelGroupInfo[] | null>(null);
|
||||
const [agentHubData, setAgentHubData] = useState<AgentCard[] | null>(null);
|
||||
const [pageTitle, setPageTitle] = useState<string>("LiteLLM Gateway");
|
||||
const [customDocsDescription, setCustomDocsDescription] = useState<string | null>(null);
|
||||
const [litellmVersion, setLitellmVersion] = useState<string>("");
|
||||
const [usefulLinks, setUsefulLinks] = useState<Record<string, string>>({});
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [agentLoading, setAgentLoading] = useState<boolean>(true);
|
||||
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||
const [agentSearchTerm, setAgentSearchTerm] = useState<string>("");
|
||||
const [selectedProviders, setSelectedProviders] = useState<string[]>([]);
|
||||
const [selectedModes, setSelectedModes] = useState<string[]>([]);
|
||||
const [selectedFeatures, setSelectedFeatures] = useState<string[]>([]);
|
||||
const [selectedAgentSkills, setSelectedAgentSkills] = useState<string[]>([]);
|
||||
const [serviceStatus, setServiceStatus] = useState<string>("I'm alive! ✓");
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [isAgentModalVisible, setIsAgentModalVisible] = useState(false);
|
||||
const [selectedModel, setSelectedModel] = useState<null | ModelGroupInfo>(null);
|
||||
const [selectedAgent, setSelectedAgent] = useState<null | AgentCard>(null);
|
||||
const [proxySettings, setProxySettings] = useState<any>({});
|
||||
const [activeTab, setActiveTab] = useState<string>("models");
|
||||
const tableRef = useRef<TableInstance<any>>(null);
|
||||
const agentTableRef = useRef<TableInstance<any>>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPublicData = async () => {
|
||||
|
|
@ -68,6 +106,19 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken }) => {
|
|||
}
|
||||
};
|
||||
|
||||
const fetchAgentData = async () => {
|
||||
try {
|
||||
setAgentLoading(true);
|
||||
const _agentHubData = await agentHubPublicModelsCall();
|
||||
console.log("AgentHubData:", _agentHubData);
|
||||
setAgentHubData(_agentHubData);
|
||||
} catch (error) {
|
||||
console.error("There was an error fetching the public agent data", error);
|
||||
} finally {
|
||||
setAgentLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchPublicModelHubInfo = async () => {
|
||||
const publicModelHubInfo = await getPublicModelHubInfo();
|
||||
console.log("Public Model Hub Info:", publicModelHubInfo);
|
||||
|
|
@ -80,6 +131,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken }) => {
|
|||
fetchPublicModelHubInfo();
|
||||
|
||||
fetchPublicData();
|
||||
fetchAgentData();
|
||||
}, []);
|
||||
|
||||
// Clear filters when filter values change to avoid confusion
|
||||
|
|
@ -123,6 +175,16 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken }) => {
|
|||
return Array.from(features).sort();
|
||||
};
|
||||
|
||||
const getUniqueAgentSkills = (data: AgentCard[]) => {
|
||||
const skills = new Set<string>();
|
||||
data.forEach((agent) => {
|
||||
agent.skills?.forEach((skill) => {
|
||||
skill.tags?.forEach((tag) => skills.add(tag));
|
||||
});
|
||||
});
|
||||
return Array.from(skills).sort();
|
||||
};
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
if (!modelHubData) return [];
|
||||
|
||||
|
|
@ -197,6 +259,57 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken }) => {
|
|||
});
|
||||
}, [modelHubData, searchTerm, selectedProviders, selectedModes, selectedFeatures]);
|
||||
|
||||
const filteredAgentData = useMemo(() => {
|
||||
if (!agentHubData) return [];
|
||||
|
||||
let searchResults = agentHubData;
|
||||
|
||||
// Apply search if there's a search term
|
||||
if (agentSearchTerm.trim()) {
|
||||
const lowercaseSearch = agentSearchTerm.toLowerCase();
|
||||
const searchWords = lowercaseSearch.split(/\s+/);
|
||||
|
||||
searchResults = agentHubData.filter((agent) => {
|
||||
const agentName = agent.name.toLowerCase();
|
||||
const agentDescription = agent.description.toLowerCase();
|
||||
|
||||
// Check if it contains the exact search term
|
||||
if (agentName.includes(lowercaseSearch) || agentDescription.includes(lowercaseSearch)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if it contains all search words
|
||||
return searchWords.every((word) => agentName.includes(word) || agentDescription.includes(word));
|
||||
});
|
||||
|
||||
// Sort by relevance
|
||||
searchResults = searchResults.sort((a, b) => {
|
||||
const aName = a.name.toLowerCase();
|
||||
const bName = b.name.toLowerCase();
|
||||
|
||||
const aExactMatch = aName === lowercaseSearch ? 1000 : 0;
|
||||
const bExactMatch = bName === lowercaseSearch ? 1000 : 0;
|
||||
|
||||
const aStartsWith = aName.startsWith(lowercaseSearch) ? 100 : 0;
|
||||
const bStartsWith = bName.startsWith(lowercaseSearch) ? 100 : 0;
|
||||
|
||||
const aScore = aExactMatch + aStartsWith + (1000 - aName.length);
|
||||
const bScore = bExactMatch + bStartsWith + (1000 - bName.length);
|
||||
|
||||
return bScore - aScore;
|
||||
});
|
||||
}
|
||||
|
||||
// Apply skill filters
|
||||
return searchResults.filter((agent) => {
|
||||
const matchesSkill =
|
||||
selectedAgentSkills.length === 0 ||
|
||||
agent.skills?.some((skill) => skill.tags?.some((tag) => selectedAgentSkills.includes(tag)));
|
||||
|
||||
return matchesSkill;
|
||||
});
|
||||
}, [agentHubData, agentSearchTerm, selectedAgentSkills]);
|
||||
|
||||
const showModal = (model: ModelGroupInfo) => {
|
||||
setSelectedModel(model);
|
||||
setIsModalVisible(true);
|
||||
|
|
@ -212,6 +325,21 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken }) => {
|
|||
setSelectedModel(null);
|
||||
};
|
||||
|
||||
const showAgentModal = (agent: AgentCard) => {
|
||||
setSelectedAgent(agent);
|
||||
setIsAgentModalVisible(true);
|
||||
};
|
||||
|
||||
const handleAgentModalOk = () => {
|
||||
setIsAgentModalVisible(false);
|
||||
setSelectedAgent(null);
|
||||
};
|
||||
|
||||
const handleAgentModalCancel = () => {
|
||||
setIsAgentModalVisible(false);
|
||||
setSelectedAgent(null);
|
||||
};
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
NotificationsManager.success("Copied to clipboard!");
|
||||
|
|
@ -446,6 +574,143 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken }) => {
|
|||
},
|
||||
];
|
||||
|
||||
const publicAgentHubColumns = (): ColumnDef<AgentCard>[] => [
|
||||
{
|
||||
header: "Agent Name",
|
||||
accessorKey: "name",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => (
|
||||
<div className="overflow-hidden">
|
||||
<Tooltip title={row.original.name}>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left"
|
||||
onClick={() => showAgentModal(row.original)}
|
||||
>
|
||||
{row.original.name}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
),
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
header: "Description",
|
||||
accessorKey: "description",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const description = row.original.description;
|
||||
const truncated = description.length > 80 ? description.substring(0, 80) + "..." : description;
|
||||
return (
|
||||
<Tooltip title={description}>
|
||||
<Text className="text-sm text-gray-700">{truncated}</Text>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
size: 250,
|
||||
},
|
||||
{
|
||||
header: "Version",
|
||||
accessorKey: "version",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => <Text className="text-sm">{row.original.version}</Text>,
|
||||
size: 80,
|
||||
},
|
||||
{
|
||||
header: "Provider",
|
||||
accessorKey: "provider",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const provider = row.original.provider;
|
||||
if (!provider) return <Text className="text-gray-400">-</Text>;
|
||||
return (
|
||||
<div className="text-sm">
|
||||
<Text className="font-medium">{provider.organization}</Text>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
size: 120,
|
||||
},
|
||||
{
|
||||
header: "Skills",
|
||||
accessorKey: "skills",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const skills = row.original.skills || [];
|
||||
if (skills.length === 0) {
|
||||
return <Text className="text-gray-400">-</Text>;
|
||||
}
|
||||
|
||||
if (skills.length === 1) {
|
||||
return (
|
||||
<div className="h-6 flex items-center">
|
||||
<Tag color="purple" className="text-xs">
|
||||
{skills[0].name}
|
||||
</Tag>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-6 flex items-center space-x-1">
|
||||
<Tag color="purple" className="text-xs">
|
||||
{skills[0].name}
|
||||
</Tag>
|
||||
<Tooltip
|
||||
title={
|
||||
<div className="space-y-1">
|
||||
<div className="font-medium">All Skills:</div>
|
||||
{skills.map((skill, index) => (
|
||||
<div key={index} className="text-xs">
|
||||
• {skill.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
trigger="click"
|
||||
placement="topLeft"
|
||||
>
|
||||
<span
|
||||
className="text-xs text-purple-600 cursor-pointer hover:text-purple-800 hover:underline"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
+{skills.length - 1}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
header: "Capabilities",
|
||||
accessorKey: "capabilities",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const capabilities = row.original.capabilities || {};
|
||||
const capList = Object.entries(capabilities)
|
||||
.filter(([_, value]) => value === true)
|
||||
.map(([key]) => key);
|
||||
|
||||
if (capList.length === 0) {
|
||||
return <Text className="text-gray-400">-</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{capList.map((cap) => (
|
||||
<Tag key={cap} color="green" className="text-xs capitalize">
|
||||
{cap}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
size: 150,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ThemeProvider accessToken={accessToken}>
|
||||
<div className="min-h-screen bg-white">
|
||||
|
|
@ -503,125 +768,202 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken }) => {
|
|||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Models Table */}
|
||||
{/* Tabs for Models and Agents */}
|
||||
<Card className="p-8 bg-white border border-gray-200 rounded-lg shadow-sm">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<Title className="text-2xl font-semibold text-gray-900">Available Models</Title>
|
||||
</div>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
size="large"
|
||||
className="public-hub-tabs"
|
||||
>
|
||||
{/* Models Tab */}
|
||||
<TabPane tab="Model Hub" key="models">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<Title className="text-2xl font-semibold text-gray-900">Available Models</Title>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8 p-6 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2 mb-3">
|
||||
<Text className="text-sm font-medium text-gray-700">Search Models:</Text>
|
||||
<Tooltip
|
||||
title="Smart search with relevance ranking - finds models containing your search terms, ranked by relevance. Try searching 'xai grok-4', 'claude-4', 'gpt-4', or 'sonnet'"
|
||||
placement="top"
|
||||
>
|
||||
<Info className="w-4 h-4 text-gray-400 cursor-help" />
|
||||
</Tooltip>
|
||||
{/* Filters */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8 p-6 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2 mb-3">
|
||||
<Text className="text-sm font-medium text-gray-700">Search Models:</Text>
|
||||
<Tooltip
|
||||
title="Smart search with relevance ranking - finds models containing your search terms, ranked by relevance. Try searching 'xai grok-4', 'claude-4', 'gpt-4', or 'sonnet'"
|
||||
placement="top"
|
||||
>
|
||||
<Info className="w-4 h-4 text-gray-400 cursor-help" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<SearchIcon className="w-4 h-4 text-gray-400 absolute left-3 top-1/2 transform -translate-y-1/2" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search model names... (smart search enabled)"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="border border-gray-300 rounded-lg pl-10 pr-4 py-2 w-full text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-sm font-medium mb-3 text-gray-700">Provider:</Text>
|
||||
<Select
|
||||
mode="multiple"
|
||||
value={selectedProviders}
|
||||
onChange={(values) => setSelectedProviders(values)}
|
||||
placeholder="Select providers"
|
||||
className="w-full"
|
||||
size="large"
|
||||
allowClear
|
||||
optionRender={(option) => {
|
||||
const { logo } = getProviderLogoAndName(option.value as string);
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
{logo && (
|
||||
<img
|
||||
src={logo}
|
||||
alt={option.label as string}
|
||||
className="w-5 h-5 flex-shrink-0 object-contain"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = "none";
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span className="capitalize">{option.label}</span>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
>
|
||||
{modelHubData &&
|
||||
getUniqueProviders(modelHubData).map((provider) => (
|
||||
<Select.Option key={provider} value={provider}>
|
||||
{provider}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-sm font-medium mb-3 text-gray-700">Mode:</Text>
|
||||
<Select
|
||||
mode="multiple"
|
||||
value={selectedModes}
|
||||
onChange={(values) => setSelectedModes(values)}
|
||||
placeholder="Select modes"
|
||||
className="w-full"
|
||||
size="large"
|
||||
allowClear
|
||||
>
|
||||
{modelHubData &&
|
||||
getUniqueModes(modelHubData).map((mode) => (
|
||||
<Select.Option key={mode} value={mode}>
|
||||
{mode}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-sm font-medium mb-3 text-gray-700">Features:</Text>
|
||||
<Select
|
||||
mode="multiple"
|
||||
value={selectedFeatures}
|
||||
onChange={(values) => setSelectedFeatures(values)}
|
||||
placeholder="Select features"
|
||||
className="w-full"
|
||||
size="large"
|
||||
allowClear
|
||||
>
|
||||
{modelHubData &&
|
||||
getUniqueFeatures(modelHubData).map((feature) => (
|
||||
<Select.Option key={feature} value={feature}>
|
||||
{feature}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<SearchIcon className="w-4 h-4 text-gray-400 absolute left-3 top-1/2 transform -translate-y-1/2" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search model names... (smart search enabled)"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="border border-gray-300 rounded-lg pl-10 pr-4 py-2 w-full text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white"
|
||||
/>
|
||||
|
||||
<ModelDataTable
|
||||
columns={publicModelHubColumns()}
|
||||
data={filteredData}
|
||||
isLoading={loading}
|
||||
table={tableRef}
|
||||
defaultSorting={[{ id: "model_group", desc: false }]}
|
||||
/>
|
||||
|
||||
<div className="mt-8 text-center">
|
||||
<Text className="text-sm text-gray-600">
|
||||
Showing {filteredData.length} of {modelHubData?.length || 0} models
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-sm font-medium mb-3 text-gray-700">Provider:</Text>
|
||||
<Select
|
||||
mode="multiple"
|
||||
value={selectedProviders}
|
||||
onChange={(values) => setSelectedProviders(values)}
|
||||
placeholder="Select providers"
|
||||
className="w-full"
|
||||
size="large"
|
||||
allowClear
|
||||
optionRender={(option) => {
|
||||
const { logo } = getProviderLogoAndName(option.value as string);
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
{logo && (
|
||||
<img
|
||||
src={logo}
|
||||
alt={option.label as string}
|
||||
className="w-5 h-5 flex-shrink-0 object-contain"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = "none";
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span className="capitalize">{option.label}</span>
|
||||
</TabPane>
|
||||
|
||||
{/* Agents Tab */}
|
||||
{agentHubData && agentHubData.length > 0 && (
|
||||
<TabPane tab="Agent Hub" key="agents">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<Title className="text-2xl font-semibold text-gray-900">Available Agents</Title>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8 p-6 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2 mb-3">
|
||||
<Text className="text-sm font-medium text-gray-700">Search Agents:</Text>
|
||||
<Tooltip
|
||||
title="Search agents by name or description"
|
||||
placement="top"
|
||||
>
|
||||
<Info className="w-4 h-4 text-gray-400 cursor-help" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
>
|
||||
{modelHubData &&
|
||||
getUniqueProviders(modelHubData).map((provider) => (
|
||||
<Select.Option key={provider} value={provider}>
|
||||
{provider}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-sm font-medium mb-3 text-gray-700">Mode:</Text>
|
||||
<Select
|
||||
mode="multiple"
|
||||
value={selectedModes}
|
||||
onChange={(values) => setSelectedModes(values)}
|
||||
placeholder="Select modes"
|
||||
className="w-full"
|
||||
size="large"
|
||||
allowClear
|
||||
>
|
||||
{modelHubData &&
|
||||
getUniqueModes(modelHubData).map((mode) => (
|
||||
<Select.Option key={mode} value={mode}>
|
||||
{mode}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-sm font-medium mb-3 text-gray-700">Features:</Text>
|
||||
<Select
|
||||
mode="multiple"
|
||||
value={selectedFeatures}
|
||||
onChange={(values) => setSelectedFeatures(values)}
|
||||
placeholder="Select features"
|
||||
className="w-full"
|
||||
size="large"
|
||||
allowClear
|
||||
>
|
||||
{modelHubData &&
|
||||
getUniqueFeatures(modelHubData).map((feature) => (
|
||||
<Select.Option key={feature} value={feature}>
|
||||
{feature}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<SearchIcon className="w-4 h-4 text-gray-400 absolute left-3 top-1/2 transform -translate-y-1/2" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search agent names or descriptions..."
|
||||
value={agentSearchTerm}
|
||||
onChange={(e) => setAgentSearchTerm(e.target.value)}
|
||||
className="border border-gray-300 rounded-lg pl-10 pr-4 py-2 w-full text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-sm font-medium mb-3 text-gray-700">Skills:</Text>
|
||||
<Select
|
||||
mode="multiple"
|
||||
value={selectedAgentSkills}
|
||||
onChange={(values) => setSelectedAgentSkills(values)}
|
||||
placeholder="Select skills"
|
||||
className="w-full"
|
||||
size="large"
|
||||
allowClear
|
||||
>
|
||||
{agentHubData &&
|
||||
getUniqueAgentSkills(agentHubData).map((skill) => (
|
||||
<Select.Option key={skill} value={skill}>
|
||||
{skill}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ModelDataTable
|
||||
columns={publicModelHubColumns()}
|
||||
data={filteredData}
|
||||
isLoading={loading}
|
||||
table={tableRef}
|
||||
defaultSorting={[{ id: "model_group", desc: false }]}
|
||||
/>
|
||||
<ModelDataTable
|
||||
columns={publicAgentHubColumns()}
|
||||
data={filteredAgentData}
|
||||
isLoading={agentLoading}
|
||||
table={agentTableRef}
|
||||
defaultSorting={[{ id: "name", desc: false }]}
|
||||
/>
|
||||
|
||||
<div className="mt-8 text-center">
|
||||
<Text className="text-sm text-gray-600">
|
||||
Showing {filteredData.length} of {modelHubData?.length || 0} models
|
||||
</Text>
|
||||
</div>
|
||||
<div className="mt-8 text-center">
|
||||
<Text className="text-sm text-gray-600">
|
||||
Showing {filteredAgentData.length} of {agentHubData?.length || 0} agents
|
||||
</Text>
|
||||
</div>
|
||||
</TabPane>
|
||||
)}
|
||||
</Tabs>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
|
|
@ -852,6 +1194,308 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken }) => {
|
|||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
{/* Agent Details Modal */}
|
||||
<Modal
|
||||
title={
|
||||
<div className="flex items-center space-x-2">
|
||||
<span>{selectedAgent?.name || "Agent Details"}</span>
|
||||
{selectedAgent && (
|
||||
<Tooltip title="Copy agent name">
|
||||
<Copy
|
||||
onClick={() => copyToClipboard(selectedAgent.name)}
|
||||
className="cursor-pointer text-gray-500 hover:text-blue-500 w-4 h-4"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
width={1000}
|
||||
open={isAgentModalVisible}
|
||||
footer={null}
|
||||
onOk={handleAgentModalOk}
|
||||
onCancel={handleAgentModalCancel}
|
||||
>
|
||||
{selectedAgent && (
|
||||
<div className="space-y-6">
|
||||
{/* Agent Overview */}
|
||||
<div>
|
||||
<Text className="text-lg font-semibold mb-4">Agent Overview</Text>
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<Text className="font-medium">Name:</Text>
|
||||
<Text>{selectedAgent.name}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Version:</Text>
|
||||
<Text>{selectedAgent.version}</Text>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Text className="font-medium">Description:</Text>
|
||||
<Text>{selectedAgent.description}</Text>
|
||||
</div>
|
||||
{selectedAgent.url && (
|
||||
<div>
|
||||
<Text className="font-medium">URL:</Text>
|
||||
<a
|
||||
href={selectedAgent.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:text-blue-800 text-sm break-all"
|
||||
>
|
||||
{selectedAgent.url}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Capabilities */}
|
||||
{selectedAgent.capabilities && (
|
||||
<div>
|
||||
<Text className="text-lg font-semibold mb-4">Capabilities</Text>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{Object.entries(selectedAgent.capabilities)
|
||||
.filter(([_, value]) => value === true)
|
||||
.map(([key]) => (
|
||||
<Tag key={key} color="green" className="capitalize">
|
||||
{key}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Skills */}
|
||||
{selectedAgent.skills && selectedAgent.skills.length > 0 && (
|
||||
<div>
|
||||
<Text className="text-lg font-semibold mb-4">Skills</Text>
|
||||
<div className="space-y-4">
|
||||
{selectedAgent.skills.map((skill, index) => (
|
||||
<div key={index} className="border border-gray-200 rounded-lg p-4">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div>
|
||||
<Text className="font-medium text-base">{skill.name}</Text>
|
||||
<Text className="text-sm text-gray-600">{skill.description}</Text>
|
||||
</div>
|
||||
</div>
|
||||
{skill.tags && skill.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{skill.tags.map((tag) => (
|
||||
<Tag key={tag} color="purple" className="text-xs">
|
||||
{tag}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Input/Output Modes */}
|
||||
<div>
|
||||
<Text className="text-lg font-semibold mb-4">Input/Output Modes</Text>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Text className="font-medium">Input Modes:</Text>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{selectedAgent.defaultInputModes?.map((mode) => (
|
||||
<Tag key={mode} color="blue">
|
||||
{mode}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Output Modes:</Text>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{selectedAgent.defaultOutputModes?.map((mode) => (
|
||||
<Tag key={mode} color="blue">
|
||||
{mode}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Documentation */}
|
||||
{selectedAgent.documentationUrl && (
|
||||
<div>
|
||||
<Text className="text-lg font-semibold mb-4">Documentation</Text>
|
||||
<a
|
||||
href={selectedAgent.documentationUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:text-blue-800 flex items-center space-x-2"
|
||||
>
|
||||
<ExternalLinkIcon className="w-4 h-4" />
|
||||
<span>View Documentation</span>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* A2A Usage Example */}
|
||||
<div>
|
||||
<Text className="text-lg font-semibold mb-4">Usage Example (A2A Protocol)</Text>
|
||||
|
||||
{/* Step 1: Retrieve Agent Card */}
|
||||
<div className="mb-4">
|
||||
<Text className="text-sm font-medium mb-2 text-gray-700">Step 1: Retrieve Agent Card</Text>
|
||||
<div className="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto">
|
||||
<pre className="text-xs">
|
||||
{`base_url = '${selectedAgent.url}'
|
||||
|
||||
resolver = A2ACardResolver(
|
||||
httpx_client=httpx_client,
|
||||
base_url=base_url,
|
||||
# agent_card_path uses default, extended_agent_card_path also uses default
|
||||
)
|
||||
|
||||
# Fetch Public Agent Card and Initialize Client
|
||||
final_agent_card_to_use: AgentCard | None = None
|
||||
_public_card = (
|
||||
await resolver.get_agent_card()
|
||||
) # Fetches from default public path - \`/agents/{agent_id}/\`
|
||||
final_agent_card_to_use = _public_card
|
||||
|
||||
if _public_card.supports_authenticated_extended_card:
|
||||
try:
|
||||
auth_headers_dict = {
|
||||
'Authorization': 'Bearer dummy-token-for-extended-card'
|
||||
}
|
||||
_extended_card = await resolver.get_agent_card(
|
||||
relative_card_path=EXTENDED_AGENT_CARD_PATH,
|
||||
http_kwargs={'headers': auth_headers_dict},
|
||||
)
|
||||
final_agent_card_to_use = (
|
||||
_extended_card # Update to use the extended card
|
||||
)
|
||||
except Exception as e_extended:
|
||||
logger.warning(
|
||||
f'Failed to fetch extended agent card: {e_extended}. Will proceed with public card.',
|
||||
exc_info=True,
|
||||
)`}
|
||||
</pre>
|
||||
</div>
|
||||
<div className="mt-2 text-right">
|
||||
<button
|
||||
onClick={() => {
|
||||
const codeSnippet = `from a2a.client import A2ACardResolver, A2AClient
|
||||
from a2a.types import (
|
||||
AgentCard,
|
||||
MessageSendParams,
|
||||
SendMessageRequest,
|
||||
SendStreamingMessageRequest,
|
||||
)
|
||||
from a2a.utils.constants import (
|
||||
AGENT_CARD_WELL_KNOWN_PATH,
|
||||
EXTENDED_AGENT_CARD_PATH,
|
||||
)
|
||||
|
||||
base_url = '${selectedAgent.url}'
|
||||
|
||||
resolver = A2ACardResolver(
|
||||
httpx_client=httpx_client,
|
||||
base_url=base_url,
|
||||
# agent_card_path uses default, extended_agent_card_path also uses default
|
||||
)
|
||||
|
||||
# Fetch Public Agent Card and Initialize Client
|
||||
final_agent_card_to_use: AgentCard | None = None
|
||||
_public_card = (
|
||||
await resolver.get_agent_card()
|
||||
) # Fetches from default public path - \`/agents/{agent_id}/\`
|
||||
final_agent_card_to_use = _public_card
|
||||
|
||||
if _public_card.supports_authenticated_extended_card:
|
||||
try:
|
||||
auth_headers_dict = {
|
||||
'Authorization': 'Bearer dummy-token-for-extended-card'
|
||||
}
|
||||
_extended_card = await resolver.get_agent_card(
|
||||
relative_card_path=EXTENDED_AGENT_CARD_PATH,
|
||||
http_kwargs={'headers': auth_headers_dict},
|
||||
)
|
||||
final_agent_card_to_use = (
|
||||
_extended_card # Update to use the extended card
|
||||
)
|
||||
except Exception as e_extended:
|
||||
logger.warning(
|
||||
f'Failed to fetch extended agent card: {e_extended}. Will proceed with public card.',
|
||||
exc_info=True,
|
||||
)`;
|
||||
copyToClipboard(codeSnippet);
|
||||
}}
|
||||
className="text-sm text-blue-600 hover:text-blue-800 cursor-pointer"
|
||||
>
|
||||
Copy to clipboard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step 2: Call the Agent */}
|
||||
<div>
|
||||
<Text className="text-sm font-medium mb-2 text-gray-700">Step 2: Call the Agent</Text>
|
||||
<div className="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto">
|
||||
<pre className="text-xs">
|
||||
{`client = A2AClient(
|
||||
httpx_client=httpx_client, agent_card=final_agent_card_to_use
|
||||
)
|
||||
|
||||
send_message_payload: dict[str, Any] = {
|
||||
'message': {
|
||||
'role': 'user',
|
||||
'parts': [
|
||||
{'kind': 'text', 'text': 'how much is 10 USD in INR?'}
|
||||
],
|
||||
'messageId': uuid4().hex,
|
||||
},
|
||||
}
|
||||
request = SendMessageRequest(
|
||||
id=str(uuid4()), params=MessageSendParams(**send_message_payload)
|
||||
)
|
||||
|
||||
response = await client.send_message(request)
|
||||
print(response.model_dump(mode='json', exclude_none=True))`}
|
||||
</pre>
|
||||
</div>
|
||||
<div className="mt-2 text-right">
|
||||
<button
|
||||
onClick={() => {
|
||||
const codeSnippet = `client = A2AClient(
|
||||
httpx_client=httpx_client, agent_card=final_agent_card_to_use
|
||||
)
|
||||
|
||||
send_message_payload: dict[str, Any] = {
|
||||
'message': {
|
||||
'role': 'user',
|
||||
'parts': [
|
||||
{'kind': 'text', 'text': 'how much is 10 USD in INR?'}
|
||||
],
|
||||
'messageId': uuid4().hex,
|
||||
},
|
||||
}
|
||||
request = SendMessageRequest(
|
||||
id=str(uuid4()), params=MessageSendParams(**send_message_payload)
|
||||
)
|
||||
|
||||
response = await client.send_message(request)
|
||||
print(response.model_dump(mode='json', exclude_none=True))`;
|
||||
copyToClipboard(codeSnippet);
|
||||
}}
|
||||
className="text-sm text-blue-600 hover:text-blue-800 cursor-pointer"
|
||||
>
|
||||
Copy to clipboard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue