|
4 | 4 | import os |
5 | 5 | import uuid |
6 | 6 | from collections import deque |
7 | | -from typing import Optional |
| 7 | +from typing import Optional, Dict |
8 | 8 |
|
9 | 9 | from fastapi import Header, Request |
10 | 10 | from fastapi.responses import JSONResponse, StreamingResponse |
|
23 | 23 | ExportAndImportDataFormat, |
24 | 24 | MCPInfo, |
25 | 25 | ToolInstanceInfoRequest, |
26 | | - ToolSourceEnum |
| 26 | + ToolSourceEnum, ModelConnectStatusEnum |
27 | 27 | ) |
28 | 28 | from database.agent_db import ( |
29 | 29 | create_agent, |
@@ -801,29 +801,140 @@ async def list_all_agent_info_impl(tenant_id: str) -> list[dict]: |
801 | 801 | try: |
802 | 802 | agent_list = query_all_agent_info_by_tenant_id(tenant_id=tenant_id) |
803 | 803 |
|
804 | | - simple_agent_list = [] |
| 804 | + model_cache: Dict[int, Optional[dict]] = {} |
| 805 | + enriched_agents: list[dict] = [] |
| 806 | + |
805 | 807 | for agent in agent_list: |
806 | | - # check agent is available |
807 | 808 | if not agent["enabled"]: |
808 | 809 | continue |
| 810 | + |
| 811 | + unavailable_reasons: list[str] = [] |
| 812 | + |
809 | 813 | tool_info = search_tools_for_sub_agent( |
810 | 814 | agent_id=agent["agent_id"], tenant_id=tenant_id) |
811 | | - tool_id_list = [tool["tool_id"] for tool in tool_info] |
812 | | - is_available = all(check_tool_is_available(tool_id_list)) |
| 815 | + tool_id_list = [tool["tool_id"] |
| 816 | + for tool in tool_info if tool.get("tool_id") is not None] |
| 817 | + if tool_id_list: |
| 818 | + tool_statuses = check_tool_is_available(tool_id_list) |
| 819 | + if not all(tool_statuses): |
| 820 | + unavailable_reasons.append("tool_unavailable") |
| 821 | + |
| 822 | + model_reasons = _collect_model_availability_reasons( |
| 823 | + agent=agent, |
| 824 | + tenant_id=tenant_id, |
| 825 | + model_cache=model_cache |
| 826 | + ) |
| 827 | + unavailable_reasons.extend(model_reasons) |
| 828 | + |
| 829 | + # Preserve the raw data so we can adjust availability for duplicates |
| 830 | + enriched_agents.append({ |
| 831 | + "raw_agent": agent, |
| 832 | + "unavailable_reasons": unavailable_reasons, |
| 833 | + }) |
| 834 | + |
| 835 | + # Handle duplicate name/display_name: keep the earliest created agent available, |
| 836 | + # mark later ones as unavailable due to duplication. |
| 837 | + _apply_duplicate_name_availability_rules(enriched_agents) |
| 838 | + |
| 839 | + simple_agent_list: list[dict] = [] |
| 840 | + for entry in enriched_agents: |
| 841 | + agent = entry["raw_agent"] |
| 842 | + unavailable_reasons = list(dict.fromkeys(entry["unavailable_reasons"])) |
813 | 843 |
|
814 | 844 | simple_agent_list.append({ |
815 | 845 | "agent_id": agent["agent_id"], |
816 | 846 | "name": agent["name"] if agent["name"] else agent["display_name"], |
817 | 847 | "display_name": agent["display_name"] if agent["display_name"] else agent["name"], |
818 | 848 | "description": agent["description"], |
819 | | - "is_available": is_available |
| 849 | + "is_available": len(unavailable_reasons) == 0, |
| 850 | + "unavailable_reasons": unavailable_reasons |
820 | 851 | }) |
| 852 | + |
821 | 853 | return simple_agent_list |
822 | 854 | except Exception as e: |
823 | 855 | logger.error(f"Failed to query all agent info: {str(e)}") |
824 | 856 | raise ValueError(f"Failed to query all agent info: {str(e)}") |
825 | 857 |
|
826 | 858 |
|
| 859 | +def _apply_duplicate_name_availability_rules(enriched_agents: list[dict]) -> None: |
| 860 | + """ |
| 861 | + For agents that share the same name or display_name, only the earliest created |
| 862 | + agent should remain available (if it has no other unavailable reasons). |
| 863 | + All later-created agents in the same group become unavailable due to duplication. |
| 864 | + """ |
| 865 | + # Group by name and display_name |
| 866 | + name_groups: dict[str, list[dict]] = {} |
| 867 | + display_name_groups: dict[str, list[dict]] = {} |
| 868 | + |
| 869 | + for entry in enriched_agents: |
| 870 | + agent = entry["raw_agent"] |
| 871 | + name = agent.get("name") |
| 872 | + if name: |
| 873 | + name_groups.setdefault(name, []).append(entry) |
| 874 | + |
| 875 | + display_name = agent.get("display_name") |
| 876 | + if display_name: |
| 877 | + display_name_groups.setdefault(display_name, []).append(entry) |
| 878 | + |
| 879 | + def _mark_duplicates(groups: dict[str, list[dict]], reason_key: str) -> None: |
| 880 | + for entries in groups.values(): |
| 881 | + if len(entries) <= 1: |
| 882 | + continue |
| 883 | + |
| 884 | + # Sort by create_time ascending so the earliest created agent comes first |
| 885 | + sorted_entries = sorted( |
| 886 | + entries, |
| 887 | + key=lambda e: e["raw_agent"].get("create_time"), |
| 888 | + ) |
| 889 | + |
| 890 | + # The first (earliest) agent keeps its current availability; |
| 891 | + # subsequent agents are marked as duplicates. |
| 892 | + for duplicate_entry in sorted_entries[1:]: |
| 893 | + duplicate_entry["unavailable_reasons"].append(reason_key) |
| 894 | + |
| 895 | + _mark_duplicates(name_groups, "duplicate_name") |
| 896 | + _mark_duplicates(display_name_groups, "duplicate_display_name") |
| 897 | + |
| 898 | + |
| 899 | +def _collect_model_availability_reasons(agent: dict, tenant_id: str, model_cache: Dict[int, Optional[dict]]) -> list[str]: |
| 900 | + """ |
| 901 | + Build a list of reasons related to model availability issues for a given agent. |
| 902 | + """ |
| 903 | + reasons: list[str] = [] |
| 904 | + reasons.extend(_check_single_model_availability( |
| 905 | + model_id=agent.get("model_id"), |
| 906 | + tenant_id=tenant_id, |
| 907 | + model_cache=model_cache, |
| 908 | + reason_key="model_unavailable" |
| 909 | + )) |
| 910 | + |
| 911 | + return reasons |
| 912 | + |
| 913 | + |
| 914 | +def _check_single_model_availability( |
| 915 | + model_id: int | None, |
| 916 | + tenant_id: str, |
| 917 | + model_cache: Dict[int, Optional[dict]], |
| 918 | + reason_key: str, |
| 919 | +) -> list[str]: |
| 920 | + if not model_id: |
| 921 | + return [] |
| 922 | + |
| 923 | + if model_id not in model_cache: |
| 924 | + model_cache[model_id] = get_model_by_model_id(model_id, tenant_id) |
| 925 | + |
| 926 | + model_info = model_cache.get(model_id) |
| 927 | + if not model_info: |
| 928 | + return [reason_key] |
| 929 | + |
| 930 | + connect_status = ModelConnectStatusEnum.get_value( |
| 931 | + model_info.get("connect_status")) |
| 932 | + if connect_status != ModelConnectStatusEnum.AVAILABLE.value: |
| 933 | + return [reason_key] |
| 934 | + |
| 935 | + return [] |
| 936 | + |
| 937 | + |
827 | 938 | def insert_related_agent_impl(parent_agent_id, child_agent_id, tenant_id): |
828 | 939 | # search the agent by bfs, check if there is a circular call |
829 | 940 | search_list = deque([child_agent_id]) |
|
0 commit comments