Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
d52afea
解决冲突
xuyaqist May 14, 2026
75e72f6
修改sql脚本名称
xuyaqist May 7, 2026
d1cf0cb
Bugfix: ssl_verify causing different result in check embedding model …
xuyaqist May 7, 2026
be804d9
Feat: support user to configurate model concurrency limit
xuyaqist May 7, 2026
1bec7c2
修改sql脚本名称
xuyaqist May 7, 2026
b4ec310
优化名称/变量名称重复提示
xuyaqist May 8, 2026
c486148
Bugfix: when creating an embedding modal, embedding_dimension_check l…
xuyaqist May 8, 2026
7618eea
Bugfix: fix the published agent version need at least one tool
xuyaqist May 8, 2026
5d511a8
Bugfix: unify agent unavaliable reason
xuyaqist May 9, 2026
741b6f2
Bugfix: use STARTTLS (TLS upgrade) when using port 587 to send email
xuyaqist May 9, 2026
b031356
新增haotian知识库路由
xuyaqist May 9, 2026
65a7c51
修复前端
xuyaqist May 9, 2026
764df4d
为nexent-config挂载证书,令容器内的 Python 应用使用宿主机的 CA 证书来验证外部 SMTP 服务器的 SSL 证书
xuyaqist May 11, 2026
3682d6b
修复模型健康检查报错
xuyaqist May 11, 2026
e38dce1
区分send email针对是否跳过证书校验的逻辑
xuyaqist May 11, 2026
d53ab23
区分sender_email和和sender_name
xuyaqist May 11, 2026
a4c17f0
修复无法获取昊天知识库列表的问题
xuyaqist May 11, 2026
0635864
Create a session with trust_env=False to ignore proxy environment var…
xuyaqist May 12, 2026
842f312
设置generate_title为非流式接口
xuyaqist May 12, 2026
9091a81
Revert "设置generate_title为非流式接口"
xuyaqist May 12, 2026
ba85471
"设置generate_title为非流式接口"
xuyaqist May 12, 2026
64dc284
设置authorization字段也为密码展示
xuyaqist May 13, 2026
680f5c7
如果是公共知识库,设置默认id
xuyaqist May 13, 2026
9d6fe0c
新增并发数量的限制
xuyaqist May 14, 2026
d7c5bdf
Bugfix: Resolve frontend cache issue when only one model is available
xuyaqist May 15, 2026
c33b368
修复循环依赖的问题
xuyaqist May 18, 2026
626f263
Bugfix: Prevent overwriting of agent name and variable name when gene…
xuyaqist May 18, 2026
9b59e72
Bugfix: Immediately show login page on 401 response
xuyaqist May 18, 2026
7c0ff33
Revert "Bugfix: Immediately show login page on 401 response"
xuyaqist May 18, 2026
dfe69ee
Bugfix: Remove invalid concurrency_limit related code from OpenAIModel
xuyaqist May 18, 2026
b7c5b82
优化代码,修复单元测试
xuyaqist May 18, 2026
6d6c6df
修改单元测试
xuyaqist May 19, 2026
ac75390
修改单元测试
xuyaqist May 19, 2026
6d63acb
修改单元测试
xuyaqist May 19, 2026
ad4a25f
修改单元测试
xuyaqist May 19, 2026
81b6928
修改单元测试
xuyaqist May 19, 2026
d13c107
修改单元测试
xuyaqist May 19, 2026
42b28b8
修改单元测试
xuyaqist May 19, 2026
0981e36
Merge branch 'develop' into xyq/feat_add_model_timeout
xuyaqist May 19, 2026
1764954
Merge branch 'develop' into xyq/feat_add_model_timeout
xuyaqist May 20, 2026
4f3c6c6
删除使用宿主机的证书
xuyaqist May 20, 2026
f6eefe5
修改sql
xuyaqist May 20, 2026
05226b3
修改UT
xuyaqist May 20, 2026
90a41f0
修改默认昊天公共知识库Id
xuyaqist May 21, 2026
b073df4
修改单元测试
xuyaqist May 21, 2026
9e05f77
删除单元测试
xuyaqist May 21, 2026
15fe84f
使用DefaultHttpxClient而非httpx.client
xuyaqist May 21, 2026
6237459
删除不存在的函数
xuyaqist May 21, 2026
6ed26ec
修改单元测试
xuyaqist May 21, 2026
2bfd1f8
修改单元测试
xuyaqist May 21, 2026
8bb6137
Merge branch 'develop' into xyq/feat_add_model_timeout
xuyaqist May 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions backend/agents/create_agent_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,9 @@ async def create_model_config_list(tenant_id):
),
url=record["base_url"],
ssl_verify=record.get("ssl_verify", True),
model_factory=record.get("model_factory")))
model_factory=record.get("model_factory"),
timeout_seconds=record.get("timeout_seconds"),
concurrency_limit=record.get("concurrency_limit")))
# fit for old version, main_model and sub_model use default model
main_model_config = tenant_config_manager.get_model_config(
key=MODEL_CONFIG_MAPPING["llm"], tenant_id=tenant_id)
Expand All @@ -258,15 +260,19 @@ async def create_model_config_list(tenant_id):
"model_name") else "",
url=main_model_config.get("base_url", ""),
ssl_verify=main_model_config.get("ssl_verify", True),
model_factory=main_model_config.get("model_factory")))
model_factory=main_model_config.get("model_factory"),
timeout_seconds=main_model_config.get("timeout_seconds"),
concurrency_limit=main_model_config.get("concurrency_limit")))
model_list.append(
ModelConfig(cite_name="sub_model",
api_key=main_model_config.get("api_key", ""),
model_name=get_model_name_from_config(main_model_config) if main_model_config.get(
"model_name") else "",
url=main_model_config.get("base_url", ""),
ssl_verify=main_model_config.get("ssl_verify", True),
model_factory=main_model_config.get("model_factory")))
model_factory=main_model_config.get("model_factory"),
timeout_seconds=main_model_config.get("timeout_seconds"),
concurrency_limit=main_model_config.get("concurrency_limit")))

return model_list

Expand Down
2 changes: 2 additions & 0 deletions backend/apps/config_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from apps.a2a_client_app import router as a2a_client_router
from apps.monitoring_app import router as monitoring_router
from apps.a2a_server_app import router as a2a_server_router
from apps.haotian_app import router as haotian_router
from consts.const import IS_SPEED_MODE
from services.prompt_template_service import sync_system_default_prompt_template

Expand Down Expand Up @@ -84,3 +85,4 @@ async def sync_default_prompt_template_on_startup():
app.include_router(invitation_router)
app.include_router(a2a_client_router)
app.include_router(a2a_server_router)
app.include_router(haotian_router)
43 changes: 43 additions & 0 deletions backend/consts/agent_unavailable_reasons.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""
Agent Unavailable Reason Constants
Comment thread
xuyaqist marked this conversation as resolved.

Centralized definition of all possible reasons why an agent may be unavailable.
These values are returned to the frontend via the 'unavailable_reasons' field.
"""


class AgentUnavailableReason:
"""Reason codes for agent unavailability."""

# Identity conflicts
DUPLICATE_NAME = "duplicate_name"
DUPLICATE_DISPLAY_NAME = "duplicate_display_name"

# Model issues
MODEL_NOT_CONFIGURED = "model_not_configured"
MODEL_UNAVAILABLE = "model_unavailable"

# Tool issues
TOOL_UNAVAILABLE = "tool_unavailable"
ALL_TOOLS_DISABLED = "all_tools_disabled"

# Agent issues
AGENT_NOT_FOUND = "agent_not_found"

@classmethod
def all_reasons(cls) -> list[str]:
"""Return all defined unavailable reason codes."""
return [
cls.DUPLICATE_NAME,
cls.DUPLICATE_DISPLAY_NAME,
cls.MODEL_NOT_CONFIGURED,
cls.MODEL_UNAVAILABLE,
cls.TOOL_UNAVAILABLE,
cls.ALL_TOOLS_DISABLED,
cls.AGENT_NOT_FOUND,
]

@classmethod
def is_valid_reason(cls, reason: str) -> bool:
"""Check if a reason string is a valid reason code."""
return reason in cls.all_reasons()
6 changes: 6 additions & 0 deletions backend/consts/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ class ModelRequest(BaseModel):
# STT specific fields
model_appid: Optional[str] = None
access_token: Optional[str] = None
timeout_seconds: Optional[int] = None
concurrency_limit: Optional[int] = None


class ProviderModelRequest(BaseModel):
Expand Down Expand Up @@ -830,6 +832,8 @@ class ManageTenantModelCreateRequest(BaseModel):
# STT specific fields
model_appid: Optional[str] = Field(None, description="Application ID for STT models (e.g., Volcano Engine)")
access_token: Optional[str] = Field(None, description="Access token for STT models (e.g., Volcano Engine)")
timeout_seconds: Optional[int] = Field(None, description="Request timeout in seconds")
concurrency_limit: Optional[int] = Field(None, description="Maximum concurrent requests for this model")


class ManageTenantModelUpdateRequest(BaseModel):
Expand All @@ -850,6 +854,8 @@ class ManageTenantModelUpdateRequest(BaseModel):
# STT specific fields
model_appid: Optional[str] = Field(None, description="Application ID for STT models")
access_token: Optional[str] = Field(None, description="Access token for STT models")
timeout_seconds: Optional[int] = Field(None, description="Request timeout in seconds")
concurrency_limit: Optional[int] = Field(None, description="Maximum concurrent requests for this model")


class ManageTenantModelDeleteRequest(BaseModel):
Expand Down
4 changes: 4 additions & 0 deletions backend/database/db_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@ class ModelRecord(TableBase):
String(100), doc="Application ID for model authentication (used by some STT/TTS providers like Volcano Engine)")
access_token = Column(
String(100), doc="Access token for model authentication (used by some STT/TTS providers like Volcano Engine)")
timeout_seconds = Column(
Integer, doc="Request timeout in seconds for this model. Default is 120 seconds.")
concurrency_limit = Column(
Integer, doc="Maximum concurrent requests for this model. Default is null (unlimited).")


class ModelMonitoringRecord(SimpleTableBase):
Expand Down
3 changes: 3 additions & 0 deletions backend/database/model_management_db.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
from typing import Any, Dict, List, Optional

from sqlalchemy import and_, desc, func, insert, select, update
Expand All @@ -7,6 +8,8 @@
from .db_models import ModelRecord
from .utils import add_creation_tracking, add_update_tracking

logger = logging.getLogger("database.model_management_db")


def create_model_record(model_data: Dict[str, Any], user_id: str, tenant_id: str) -> bool:
"""
Expand Down
11 changes: 6 additions & 5 deletions backend/services/agent_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from consts.const import MEMORY_SEARCH_START_MSG, MEMORY_SEARCH_DONE_MSG, MEMORY_SEARCH_FAIL_MSG, TOOL_TYPE_MAPPING, \
LANGUAGE, MESSAGE_ROLE, MODEL_CONFIG_MAPPING, CAN_EDIT_ALL_USER_ROLES, PERMISSION_EDIT, PERMISSION_READ, PERMISSION_PRIVATE
from consts.exceptions import MemoryPreparationException
from consts.agent_unavailable_reasons import AgentUnavailableReason
from consts.model import (
AgentInfoRequest,
AgentRequest,
Expand Down Expand Up @@ -1585,8 +1586,8 @@ def _mark_duplicates(groups: dict[str, list[dict]], reason_key: str) -> None:
for duplicate_entry in sorted_entries[1:]:
duplicate_entry["unavailable_reasons"].append(reason_key)

_mark_duplicates(name_groups, "duplicate_name")
_mark_duplicates(display_name_groups, "duplicate_display_name")
_mark_duplicates(name_groups, AgentUnavailableReason.DUPLICATE_NAME)
_mark_duplicates(display_name_groups, AgentUnavailableReason.DUPLICATE_DISPLAY_NAME)


def _collect_model_availability_reasons(agent: dict, tenant_id: str, model_cache: Dict[int, Optional[dict]]) -> list[str]:
Expand All @@ -1598,7 +1599,7 @@ def _collect_model_availability_reasons(agent: dict, tenant_id: str, model_cache
model_id=agent.get("model_id"),
tenant_id=tenant_id,
model_cache=model_cache,
reason_key="model_unavailable"
reason_key=AgentUnavailableReason.MODEL_UNAVAILABLE
))

return reasons
Expand Down Expand Up @@ -1656,15 +1657,15 @@ def check_agent_availability(
agent_info = search_agent_info_by_agent_id(agent_id, tenant_id)

if not agent_info:
return False, ["agent_not_found"]
return False, [AgentUnavailableReason.AGENT_NOT_FOUND]

# Check tool availability
tool_info = search_tools_for_sub_agent(agent_id=agent_id, tenant_id=tenant_id)
tool_id_list = [tool["tool_id"] for tool in tool_info if tool.get("tool_id") is not None]
if tool_id_list:
tool_statuses = check_tool_is_available(tool_id_list)
if not all(tool_statuses):
unavailable_reasons.append("tool_unavailable")
unavailable_reasons.append(AgentUnavailableReason.TOOL_UNAVAILABLE)

# Check model availability
model_reasons = _collect_model_availability_reasons(
Expand Down
14 changes: 6 additions & 8 deletions backend/services/agent_version_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
)
from database.model_management_db import get_model_by_model_id
from utils.str_utils import convert_string_to_list
from consts.agent_unavailable_reasons import AgentUnavailableReason

logger = logging.getLogger("agent_version_service")

Expand Down Expand Up @@ -337,21 +338,18 @@ def _check_version_snapshot_availability(

# Check if agent info exists
if not agent_info:
return False, ["agent_not_found"]
return False, [AgentUnavailableReason.AGENT_NOT_FOUND]

# Check model availability
model_id = agent_info.get('model_id')
if model_id is None or model_id == 0:
unavailable_reasons.append("model_not_configured")
unavailable_reasons.append(AgentUnavailableReason.MODEL_NOT_CONFIGURED)

# Check tools availability
if not tool_instances:
unavailable_reasons.append("no_tools")
else:
# Check if at least one tool is enabled
# Check tools availability (only when tools are configured)
if tool_instances:
has_enabled_tool = any(t.get('enabled', True) for t in tool_instances)
if not has_enabled_tool:
unavailable_reasons.append("all_tools_disabled")
unavailable_reasons.append(AgentUnavailableReason.ALL_TOOLS_DISABLED)

return len(unavailable_reasons) == 0, unavailable_reasons

Expand Down
6 changes: 5 additions & 1 deletion backend/services/conversation_management_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,8 @@ def call_llm_for_title(question: str, tenant_id: str, language: str = LANGUAGE["
display_name = model_config.get("display_name", "") if model_config else ""
set_monitoring_operation("title_generation", display_name=display_name or None)

timeout_seconds = model_config.get("timeout_seconds") if model_config else None

# Create OpenAIModel instance
llm = OpenAIModel(
model_id=get_model_name_from_config(model_config) if model_config.get("model_name") else "",
Expand All @@ -256,7 +258,9 @@ def call_llm_for_title(question: str, tenant_id: str, language: str = LANGUAGE["
temperature=0.7,
top_p=0.95,
model_factory=model_config.get("model_factory", None),
ssl_verify=model_config.get("ssl_verify", True)
ssl_verify=model_config.get("ssl_verify", True),
timeout_seconds=timeout_seconds,
stream=False,
)

# Build messages - use new template variable 'question' instead of 'content'
Expand Down
2 changes: 2 additions & 0 deletions backend/services/file_management_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,13 +352,15 @@ def get_llm_model(tenant_id: str):
# Get the tenant config
main_model_config = tenant_config_manager.get_model_config(
key=MODEL_CONFIG_MAPPING["llm"], tenant_id=tenant_id)
timeout_seconds = main_model_config.get("timeout_seconds") if main_model_config else None
long_text_to_text_model = OpenAILongContextModel(
observer=MessageObserver(),
model_id=get_model_name_from_config(main_model_config),
api_base=main_model_config.get("base_url"),
api_key=main_model_config.get("api_key"),
max_context_tokens=main_model_config.get("max_tokens"),
ssl_verify=main_model_config.get("ssl_verify", True),
timeout_seconds=timeout_seconds,
)
return long_text_to_text_model

Expand Down
19 changes: 12 additions & 7 deletions backend/services/haotian_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

logger = logging.getLogger("haotian_service")

_DEFAULT_KNOWLEDGE_BASE_ID = "a8d68fbf-bd6e-5461-a9d1-cf1bb3522e38"
Comment thread
WMC001 marked this conversation as resolved.


def _normalize_list_payload(raw: Dict[str, Any]) -> Dict[str, Any]:
"""
Expand All @@ -24,7 +26,7 @@ def _normalize_list_payload(raw: Dict[str, Any]) -> Dict[str, Any]:
]
}

This function also filters out knowledge sets with name == "Public".
When dify_dataset_id is "null", it is replaced with the default ID.
"""
knowledge_sets = raw.get("knowledge_sets", [])
if not isinstance(knowledge_sets, list):
Expand All @@ -35,7 +37,7 @@ def _normalize_list_payload(raw: Dict[str, Any]) -> Dict[str, Any]:
if not isinstance(ks, dict):
continue
set_name = str(ks.get("name", "") or "").strip()
if not set_name or set_name == "Public":
if not set_name:
continue

bases = ks.get("knowledge_bases", [])
Expand All @@ -48,15 +50,18 @@ def _normalize_list_payload(raw: Dict[str, Any]) -> Dict[str, Any]:
continue
dataset_id = str(kb.get("dify_dataset_id", "") or "").strip()
kb_name = str(kb.get("name", "") or "").strip()
if not dataset_id or not kb_name:
if not kb_name:
continue
if dataset_id == "null" or not dataset_id:
dataset_id = _DEFAULT_KNOWLEDGE_BASE_ID
normalized_bases.append(
{"dify_dataset_id": dataset_id, "name": kb_name}
)

normalized_sets.append(
{"name": set_name, "knowledge_bases": normalized_bases}
)
if normalized_bases:
normalized_sets.append(
{"name": set_name, "knowledge_bases": normalized_bases}
)

return {"knowledge_sets": normalized_sets}

Expand All @@ -77,7 +82,7 @@ async def fetch_haotian_knowledge_sets_impl(
)

headers = {"Authorization": external_authorization}
async with httpx.AsyncClient(timeout=timeout_s, follow_redirects=True) as client:
async with httpx.AsyncClient(timeout=timeout_s, follow_redirects=True, trust_env=False) as client:
resp = await client.get(list_url, headers=headers)
if resp.status_code >= 400:
raise RuntimeError(
Expand Down
Loading
Loading