Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
02af057
解决冲突
xuyaqist May 14, 2026
9549ab1
修改sql脚本名称
xuyaqist May 7, 2026
3aff4b0
Bugfix: ssl_verify causing different result in check embedding model …
xuyaqist May 7, 2026
b9bbac0
Feat: support user to configurate model concurrency limit
xuyaqist May 7, 2026
32fd95a
修改sql脚本名称
xuyaqist May 7, 2026
6656fb3
优化名称/变量名称重复提示
xuyaqist May 8, 2026
902dabd
Bugfix: when creating an embedding modal, embedding_dimension_check l…
xuyaqist May 8, 2026
41de763
Bugfix: fix the published agent version need at least one tool
xuyaqist May 8, 2026
36b780c
Bugfix: unify agent unavaliable reason
xuyaqist May 9, 2026
ac5b53f
Bugfix: use STARTTLS (TLS upgrade) when using port 587 to send email
xuyaqist May 9, 2026
4a14cf5
新增haotian知识库路由
xuyaqist May 9, 2026
6bfb411
修复前端
xuyaqist May 9, 2026
f97c4be
为nexent-config挂载证书,令容器内的 Python 应用使用宿主机的 CA 证书来验证外部 SMTP 服务器的 SSL 证书
xuyaqist May 11, 2026
3720bfe
修复模型健康检查报错
xuyaqist May 11, 2026
134f98b
区分send email针对是否跳过证书校验的逻辑
xuyaqist May 11, 2026
225c42b
区分sender_email和和sender_name
xuyaqist May 11, 2026
111785b
修复无法获取昊天知识库列表的问题
xuyaqist May 11, 2026
53444fc
Create a session with trust_env=False to ignore proxy environment var…
xuyaqist May 12, 2026
df35e42
设置generate_title为非流式接口
xuyaqist May 12, 2026
4591682
Revert "设置generate_title为非流式接口"
xuyaqist May 12, 2026
eaed3ca
"设置generate_title为非流式接口"
xuyaqist May 12, 2026
a49e4fd
设置authorization字段也为密码展示
xuyaqist May 13, 2026
30b796f
如果是公共知识库,设置默认id
xuyaqist May 13, 2026
0a433db
新增并发数量的限制
xuyaqist May 14, 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 @@ -29,6 +29,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

# Create logger instance
Expand Down Expand Up @@ -71,3 +72,4 @@
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

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 @@ -121,6 +121,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 @@ -755,6 +757,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 @@ -775,6 +779,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
55 changes: 55 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 Expand Up @@ -84,6 +87,58 @@ def update_model_record(
return result.rowcount > 0


def update_model_record_by_model_name(
model_name: str,
update_data: Dict[str, Any],
user_id: Optional[str] = None,
tenant_id: Optional[str] = None,
model_repo: Optional[str] = None
) -> bool:
"""
Update a model record by model_name and tenant_id.

Args:
model_name: Model name (display name, not the primary key)
update_data: Dictionary containing update data
user_id: Reserved parameter for filling updated_by field
tenant_id: Tenant ID for filtering
model_repo: Optional model repo for more precise matching

Returns:
bool: Whether the operation was successful
"""
import logging
db_logger = logging.getLogger("database.client")

with get_db_session() as session:
# Data cleaning
cleaned_data = db_client.clean_string_values(update_data)

# Add update timestamp
cleaned_data["update_time"] = func.current_timestamp()
if user_id:
cleaned_data = add_update_tracking(cleaned_data, user_id)

db_logger.debug(f"update_model_record_by_model_name: model_name={model_name}, model_repo={model_repo}, tenant_id={tenant_id}, cleaned_data={cleaned_data}")

# Build conditions list
conditions = [
ModelRecord.model_name == model_name,
ModelRecord.tenant_id == tenant_id
]
Comment on lines +124 to +128
if model_repo:
conditions.append(ModelRecord.model_repo == model_repo)

# Build the update statement
stmt = update(ModelRecord).where(*conditions).values(cleaned_data)

# Execute the update statement
result = session.execute(stmt)
db_logger.info(f"update_model_record_by_model_name: rowcount={result.rowcount}")

return result.rowcount > 0


def delete_model_record(model_id: int, user_id: str, tenant_id: str) -> bool:
"""
Delete a model record (soft delete) and update the update timestamp
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 @@ -19,6 +19,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 @@ -1489,8 +1490,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 @@ -1502,7 +1503,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 @@ -1560,15 +1561,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 = "abcdefg"


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