Skip to content

Commit 59141d0

Browse files
committed
Merge remote-tracking branch 'origin/develop' into clf/lianyong_asset_owner
# Conflicts: # frontend/app/[locale]/tenant-resources/components/UserManageComp.tsx
2 parents e09ae3b + 31a8966 commit 59141d0

104 files changed

Lines changed: 16677 additions & 304 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,15 @@ model-assets/
5050
openspec/
5151
logs/
5252

53+
.agents/
5354
.devspace/
5455
devspace.yaml
5556
k8s/helm/**/*.tgz
5657
k8s/helm/nexent/Chart.lock
5758

5859
MAC_DEVELOPMENT_GUIDE.md
59-
# Mac本地开发数据持久化(无需提交)
6060
data/
61+
sdk/benchmark/.env
6162
/docker/.env.bak
63+
64+
.venv

backend/agents/create_agent_info.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from utils.model_name_utils import add_repo_to_name
3232
from utils.prompt_template_utils import get_agent_prompt_template
3333
from utils.config_utils import tenant_config_manager, get_model_name_from_config
34+
from utils.context_utils import build_context_components
3435
from consts.const import LOCAL_MCP_SERVER, MODEL_CONFIG_MAPPING, LANGUAGE, DATA_PROCESS_SERVICE, MINIO_DEFAULT_BUCKET
3536
from consts.exceptions import ValidationError
3637

@@ -412,6 +413,9 @@ async def create_agent_config(
412413
# Get skills list for prompt template
413414
skills = _get_skills_for_template(agent_id, tenant_id, version_no)
414415

416+
time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
417+
is_manager = len(managed_agents) > 0 or len(external_a2a_agents) > 0
418+
415419
render_kwargs = {
416420
"duty": duty_prompt,
417421
"constraint": constraint_prompt,
@@ -424,11 +428,30 @@ async def create_agent_config(
424428
"APP_DESCRIPTION": app_description,
425429
"memory_list": memory_list,
426430
"knowledge_base_summary": knowledge_base_summary,
427-
"time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
431+
"time": time_str,
428432
"user_id": user_id,
429433
}
430434
system_prompt = Template(prompt_template["system_prompt"], undefined=StrictUndefined).render(render_kwargs)
431435

436+
context_components = build_context_components(
437+
duty=duty_prompt,
438+
constraint=constraint_prompt,
439+
few_shots=few_shots_prompt,
440+
app_name=app_name,
441+
app_description=app_description,
442+
time_str=time_str,
443+
user_id=user_id,
444+
language=language,
445+
is_manager=is_manager,
446+
tools=render_kwargs["tools"],
447+
skills=skills,
448+
managed_agents=render_kwargs["managed_agents"],
449+
external_a2a_agents=render_kwargs["external_a2a_agents"],
450+
memory_list=memory_list,
451+
memory_search_query=last_user_query,
452+
knowledge_base_summary=knowledge_base_summary,
453+
)
454+
432455
model_id_to_use = override_model_id if override_model_id else agent_info.get("model_id")
433456
model_max_tokens = 10000
434457
if model_id_to_use is not None:
@@ -454,12 +477,13 @@ async def create_agent_config(
454477
agent_id=agent_id
455478
),
456479
tools=tool_list + _get_skill_script_tools(agent_id, tenant_id, version_no),
457-
max_steps=agent_info.get("max_steps", 10),
480+
max_steps=agent_info.get("max_steps", 15),
458481
model_name=model_name,
459482
provide_run_summary=agent_info.get("provide_run_summary", False),
460483
managed_agents=managed_agents,
461484
external_a2a_agents=external_a2a_agents,
462-
context_manager_config=cm_config
485+
context_manager_config=cm_config,
486+
context_components=context_components,
463487
)
464488
return agent_config
465489

backend/apps/agent_app.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,11 @@ async def agent_run_api(agent_request: AgentRequest, http_request: Request, auth
6464
)
6565
except Exception as e:
6666
logger.error(f"Agent run error: {str(e)}")
67+
# Only expose actual error in debug mode for better diagnosis
68+
# Keep generic message in normal mode for user experience
69+
error_detail = str(e) if agent_request.is_debug else "Agent run error."
6770
raise HTTPException(
68-
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Agent run error.")
71+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=error_detail)
6972

7073

7174
@agent_runtime_router.get("/stop/{conversation_id}")

backend/consts/model.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,7 @@ class AgentInfoRequest(BaseModel):
437437
author: Optional[str] = None
438438
model_name: Optional[str] = None
439439
model_id: Optional[int] = None
440-
max_steps: Optional[int] = None
440+
max_steps: Optional[int] = Field(default=None, ge=1, le=30)
441441
provide_run_summary: Optional[bool] = None
442442
duty_prompt: Optional[str] = None
443443
constraint_prompt: Optional[str] = None

backend/database/agent_db.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ def create_agent(agent_info, tenant_id: str, user_id: str):
162162
:return: Created agent object
163163
"""
164164
info_with_metadata = dict(agent_info)
165-
info_with_metadata.setdefault("max_steps", 5)
165+
info_with_metadata.setdefault("max_steps", 15)
166166
info_with_metadata.update({
167167
"tenant_id": tenant_id,
168168
"version_no": 0, # Default to draft version

backend/services/agent_service.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1426,9 +1426,9 @@ async def import_agent_by_agent_id(
14261426
enabled=True,
14271427
params=tool.params))
14281428
# check the validity of the agent parameters
1429-
if import_agent_info.max_steps <= 0 or import_agent_info.max_steps > 20:
1429+
if import_agent_info.max_steps <= 0 or import_agent_info.max_steps > 30:
14301430
raise ValueError(
1431-
f"Invalid max steps: {import_agent_info.max_steps}. max steps must be greater than 0 and less than 20.")
1431+
f"Invalid max steps: {import_agent_info.max_steps}. max steps must be greater than 0 and less than 30.")
14321432
if not import_agent_info.name.isidentifier():
14331433
raise ValueError(
14341434
f"Invalid agent name: {import_agent_info.name}. agent name must be a valid python variable name.")

backend/services/agent_version_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -820,7 +820,7 @@ async def list_published_agents_impl(
820820
# Apply visibility filter for DEV/USER based on group overlap
821821
if not can_edit_all:
822822
agent_group_ids = set(convert_string_to_list(agent.get("group_ids")))
823-
is_creator = str(agent.get("created_by)) == str(user_id)"))
823+
is_creator = str(agent.get("created_by")) == str(user_id)
824824
if not is_creator and len(user_group_ids.intersection(agent_group_ids)) == 0:
825825
continue
826826

backend/services/model_health_service.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515

1616
logger = logging.getLogger("model_health_service")
1717

18+
DASHSCOPE_MODEL_FACTORY = "dashscope"
19+
TOKENPONY_MODEL_FACTORY = "tokenpony"
20+
PROVIDER_CATALOG_HEALTHCHECK_FACTORIES = {DASHSCOPE_MODEL_FACTORY, TOKENPONY_MODEL_FACTORY}
21+
PROVIDER_CATALOG_HEALTHCHECK_TYPES = {"vlm", "vlm2", "vlm3"}
22+
1823

1924
def _mask_secret(value: Optional[str]) -> str:
2025
"""Mask a secret value, showing only first and last 4 characters."""
@@ -64,6 +69,31 @@ async def _embedding_dimension_check(
6469
raise ValueError(f"Unsupported model type: {model_type}")
6570

6671

72+
async def _provider_catalog_connectivity_check(
73+
model_name: str,
74+
model_type: str,
75+
model_api_key: str,
76+
model_factory: Optional[str],
77+
) -> bool:
78+
"""Validate provider-managed multimodal models through their model catalog."""
79+
provider = (model_factory or "").lower()
80+
if provider not in PROVIDER_CATALOG_HEALTHCHECK_FACTORIES:
81+
return False
82+
83+
from services.model_provider_service import get_provider_models
84+
85+
model_list = await get_provider_models({
86+
"provider": provider,
87+
"model_type": model_type,
88+
"api_key": model_api_key,
89+
})
90+
if not model_list or any(model.get("_error") for model in model_list):
91+
return False
92+
93+
expected_model_id = model_name.lower()
94+
return any(str(model.get("id", "")).lower() == expected_model_id for model in model_list)
95+
96+
6797
async def _perform_connectivity_check(
6898
model_name: str,
6999
model_type: str,
@@ -135,6 +165,18 @@ async def _perform_connectivity_check(
135165
)
136166
connectivity = await rerank_model.connectivity_check()
137167
elif model_type in ("vlm", "vlm2", "vlm3"):
168+
if (
169+
model_type in PROVIDER_CATALOG_HEALTHCHECK_TYPES
170+
and (model_factory or "").lower() in PROVIDER_CATALOG_HEALTHCHECK_FACTORIES
171+
):
172+
connectivity = await _provider_catalog_connectivity_check(
173+
model_name=model_name,
174+
model_type=model_type,
175+
model_api_key=model_api_key,
176+
model_factory=model_factory,
177+
)
178+
return connectivity
179+
138180
observer = MessageObserver()
139181
set_monitoring_operation("connectivity_check",
140182
display_name=display_name)

backend/services/model_management_service.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from database.model_management_db import (
99
create_model_record,
1010
delete_model_record,
11-
get_model_by_display_name,
1211
get_model_by_name_factory,
1312
get_models_by_display_name,
1413
get_model_records,
@@ -32,6 +31,23 @@
3231

3332
logger = logging.getLogger("model_management_service")
3433

34+
INDEPENDENT_MULTIMODAL_MODEL_TYPES = {"vlm", "vlm2", "vlm3"}
35+
36+
37+
def _has_display_name_conflict(existing_models: List[Dict[str, Any]], model_type: Optional[str]) -> bool:
38+
"""Allow the three multimodal slots to share display names across slots."""
39+
if not existing_models:
40+
return False
41+
42+
if model_type in INDEPENDENT_MULTIMODAL_MODEL_TYPES:
43+
return any(
44+
existing.get("model_type") == model_type
45+
or existing.get("model_type") not in INDEPENDENT_MULTIMODAL_MODEL_TYPES
46+
for existing in existing_models
47+
)
48+
49+
return True
50+
3551

3652
async def create_model_for_tenant(user_id: str, tenant_id: str, model_data: Dict[str, Any]):
3753
"""Create a single model record for the given tenant.
@@ -77,9 +93,9 @@ async def create_model_for_tenant(user_id: str, tenant_id: str, model_data: Dict
7793

7894
# Check display name conflict scoped by tenant
7995
if model_data.get("display_name"):
80-
existing_model_by_display = get_model_by_display_name(
96+
existing_models_by_display = get_models_by_display_name(
8197
model_data["display_name"], tenant_id)
82-
if existing_model_by_display:
98+
if _has_display_name_conflict(existing_models_by_display, model_data.get("model_type")):
8399
logging.error(
84100
f"Name {model_data['display_name']} is already in use, please choose another display name")
85101
raise ValueError(

backend/services/providers/dashscope_provider.py

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,75 @@
66
from services.providers.base import AbstractModelProvider, _classify_provider_error
77

88

9+
DASHSCOPE_IMAGE_GENERATION_KEYWORDS = (
10+
"image",
11+
"wanx",
12+
"aitryon",
13+
"tryon",
14+
"flux",
15+
"stable-diffusion",
16+
"sdxl",
17+
)
18+
DASHSCOPE_IMAGE_UNDERSTANDING_KEYWORDS = (
19+
"qwen-vl",
20+
"qwen2-vl",
21+
"qwen2.5-vl",
22+
"qwen3-vl",
23+
"qwen3.5-vl",
24+
"qwen3.6-vl",
25+
"-vl",
26+
"vl-",
27+
"vision",
28+
"visual",
29+
"ocr",
30+
"qwen3.6",
31+
"qwen-3.6",
32+
)
33+
DASHSCOPE_VIDEO_UNDERSTANDING_KEYWORDS = ("omni", "video-understanding", "video-ocr")
34+
35+
36+
def _modality_set(value) -> set:
37+
if not value:
38+
return set()
39+
if isinstance(value, str):
40+
return {value.lower()}
41+
return {str(item).lower() for item in value}
42+
43+
44+
def _has_keyword(text: str, keywords: tuple) -> bool:
45+
return any(keyword in text for keyword in keywords)
46+
47+
48+
def _is_dashscope_explicit_image_understanding_model(model_id: str) -> bool:
49+
return _has_keyword(model_id, DASHSCOPE_IMAGE_UNDERSTANDING_KEYWORDS)
50+
51+
52+
def _is_dashscope_image_generation_model(model_id: str, desc: str, req_mods: set, res_mods: set) -> bool:
53+
if _is_dashscope_explicit_image_understanding_model(model_id):
54+
return False
55+
return "image" in res_mods or _has_keyword(model_id, DASHSCOPE_IMAGE_GENERATION_KEYWORDS)
56+
57+
58+
def _is_dashscope_video_understanding_model(model_id: str, desc: str, req_mods: set, res_mods: set) -> bool:
59+
searchable_text = f"{model_id} {desc.lower()}"
60+
if "video" in req_mods and "text" in res_mods:
61+
return True
62+
return _has_keyword(searchable_text, DASHSCOPE_VIDEO_UNDERSTANDING_KEYWORDS)
63+
64+
65+
def _is_dashscope_image_understanding_model(model_id: str, desc: str, req_mods: set, res_mods: set) -> bool:
66+
searchable_text = f"{model_id} {desc.lower()}"
67+
if _is_dashscope_image_generation_model(model_id, desc, req_mods, res_mods):
68+
return False
69+
if _is_dashscope_video_understanding_model(model_id, desc, req_mods, res_mods):
70+
return False
71+
if ("image" in req_mods or "video" in req_mods) and "text" in res_mods:
72+
return True
73+
return _is_dashscope_explicit_image_understanding_model(model_id) or _has_keyword(
74+
searchable_text, DASHSCOPE_IMAGE_UNDERSTANDING_KEYWORDS
75+
)
76+
77+
978
class DashScopeModelProvider(AbstractModelProvider):
1079
"""Concrete implementation for DashScope (Aliyun) provider."""
1180

@@ -57,6 +126,8 @@ async def get_models(self, provider_config: Dict) -> List[Dict]:
57126
categorized_models = {
58127
"chat": [], # Maps to "llm"
59128
"vlm": [], # Maps to "vlm"
129+
"vlm2": [], # Maps to image generation models
130+
"vlm3": [], # Maps to video understanding models
60131
"embedding": [], # Maps to "embedding" / "multi_embedding"
61132
"rerank": [], # Maps to "rerank"
62133
"tts": [], # Maps to "tts"
@@ -71,6 +142,8 @@ async def get_models(self, provider_config: Dict) -> List[Dict]:
71142
metadata = model_obj.get('inference_metadata') or {}
72143
req_mod = metadata.get('request_modality', [])
73144
res_mod = metadata.get('response_modality', [])
145+
req_mods = _modality_set(req_mod)
146+
res_mods = _modality_set(res_mod)
74147
model_obj.setdefault("object", model_obj.get("object", "model"))
75148
model_obj.setdefault("owned_by", model_obj.get("owned_by", "dashscope"))
76149
cleaned_model = {
@@ -107,8 +180,17 @@ async def get_models(self, provider_config: Dict) -> List[Dict]:
107180
continue
108181

109182
# 5. VLM
110-
vision_mods = {'Image', 'Video'}
111-
if (set(req_mod) & vision_mods) or (set(res_mod) & vision_mods) or '视觉' in desc:
183+
if _is_dashscope_video_understanding_model(m_id, desc, req_mods, res_mods):
184+
cleaned_model.update({"model_tag": "chat", "model_type": "vlm3"})
185+
categorized_models['vlm3'].append(cleaned_model)
186+
continue
187+
188+
if _is_dashscope_image_generation_model(m_id, desc, req_mods, res_mods):
189+
cleaned_model.update({"model_tag": "chat", "model_type": "vlm2"})
190+
categorized_models['vlm2'].append(cleaned_model)
191+
continue
192+
193+
if _is_dashscope_image_understanding_model(m_id, desc, req_mods, res_mods):
112194
cleaned_model.update({"model_tag": "chat", "model_type": "vlm"})
113195
categorized_models['vlm'].append(cleaned_model)
114196
continue
@@ -124,7 +206,10 @@ async def get_models(self, provider_config: Dict) -> List[Dict]:
124206
elif target_model_type in ("embedding", "multi_embedding"):
125207
return categorized_models["embedding"]
126208
elif target_model_type in categorized_models:
127-
return categorized_models[target_model_type]
209+
return [
210+
{**model, "model_type": target_model_type}
211+
for model in categorized_models[target_model_type]
212+
]
128213
else:
129214
return []
130215
except (httpx.HTTPStatusError, httpx.ConnectTimeout, httpx.ConnectError, Exception) as e:

0 commit comments

Comments
 (0)