Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 10 additions & 4 deletions backend/adapters/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
from adapters.exception import JiuwenSDKError, JiuwenSDKUnavailableError, NexentCapabilityError

try:
from adapters.jiuwen_sdk_adapter import JiuwenSDKAdapter
except ModuleNotFoundError:
JiuwenSDKAdapter = None # type: ignore[assignment, misc]

def __getattr__(name: str):
"""Load the optional OpenJiuwen evaluation adapter only when requested."""
if name != "JiuwenSDKAdapter":
raise AttributeError(name)
try:
from adapters.jiuwen_sdk_adapter import JiuwenSDKAdapter
except ModuleNotFoundError:
return None
return JiuwenSDKAdapter

__all__ = [
"JiuwenSDKError",
Expand Down
150 changes: 144 additions & 6 deletions backend/agents/create_agent_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@

from jinja2 import Template, StrictUndefined
from nexent.core.utils.observer import MessageObserver
from nexent.core.agents.agent_model import AgentRunInfo, ModelConfig, AgentConfig, ToolConfig, ExternalA2AAgentConfig, AgentHistory, AgentVerificationConfig
from nexent.core.agents.agent_model import (
AgentConfig,
AgentHistory,
AgentRunInfo,
AgentVerificationConfig,
ExternalA2AAgentConfig,
ModelConfig,
ToolConfig,
)
from nexent.core.agents.summary_config import ContextManagerConfig
from nexent.core.models.prompt_cache import resolve_prompt_cache_profile
from nexent.core.models.capacity_resolver import (
Expand Down Expand Up @@ -53,7 +61,7 @@
from utils.context_utils import build_context_components
from utils.redis_utils import get_redis_client
from consts.const import LOCAL_MCP_SERVER, MODEL_CONFIG_MAPPING, LANGUAGE, DATA_PROCESS_SERVICE, MINIO_DEFAULT_BUCKET
from consts.model import AgentToolParamsRequest, ToolParamsRequest
from consts.model import ToolParamsRequest
from consts.exceptions import ValidationError

logger = logging.getLogger("create_agent_info")
Expand Down Expand Up @@ -527,14 +535,16 @@ def _get_external_a2a_agents(
def _get_skill_script_tools(
agent_id: int,
tenant_id: str,
version_no: int = 0
version_no: int = 0,
allowed_skills: Optional[List[dict]] = None,
) -> List[ToolConfig]:
"""Get tool config for skill script execution and skill reading.

Args:
agent_id: Agent ID for filtering available skills in error messages.
tenant_id: Tenant ID for filtering available skills in error messages.
version_no: Version number for filtering available skills.
allowed_skills: Skill summaries already authorized during agent assembly.

Returns:
List of ToolConfig for skill execution and reading tools
Expand All @@ -546,6 +556,14 @@ def _get_skill_script_tools(
"tenant_id": tenant_id,
"version_no": version_no,
}
if allowed_skills is not None:
skill_context["allowed_skills"] = sorted(
{
str(skill.get("name") or "").strip()
for skill in allowed_skills
if str(skill.get("name") or "").strip()
}
)

try:
return [
Expand Down Expand Up @@ -706,10 +724,43 @@ async def create_agent_config(
request_requested_output_tokens: int | None = None,
tool_params: Optional[ToolParamsRequest | Dict[str, Any]] = None,
enable_planning: bool = False,
_ancestry: tuple[int, ...] = (),
_expected_runtime_framework: str | None = None,
):
normalized_tool_params = _normalize_tool_params_request(tool_params)
agent_info = search_agent_info_by_agent_id(
agent_id=agent_id, tenant_id=tenant_id, version_no=version_no)
if agent_id in _ancestry:
raise ValidationError(
f"Circular dependency detected while assembling Agent {agent_id}."
)
if "runtime_framework" not in agent_info:
runtime_framework = "smolagents"
else:
runtime_framework = agent_info.get("runtime_framework")
if runtime_framework is None:
from consts.error_code import ErrorCode
from consts.exceptions import AppException

raise AppException(
ErrorCode.AGENT_RUNTIME_FRAMEWORK_REQUIRED,
f"Agent {agent_id} must select a runtime framework before it can run.",
)
if runtime_framework not in {"smolagents", "openjiuwen"}:
raise ValidationError(
f"Agent {agent_id} has unsupported runtime framework: {runtime_framework}."
)
if (
_expected_runtime_framework is not None
and runtime_framework != _expected_runtime_framework
):
from consts.error_code import ErrorCode
from consts.exceptions import AppException

raise AppException(
ErrorCode.AGENT_RUNTIME_FRAMEWORK_MISMATCH,
"Internal parent and child Agents must use the same runtime framework.",
)

# create sub agent
sub_agent_relations = query_sub_agent_relations(
Expand All @@ -732,6 +783,8 @@ async def create_agent_config(
version_no=sub_agent_version_no,
override_model_id=None,
tool_params=normalized_tool_params,
_ancestry=(*_ancestry, agent_id),
_expected_runtime_framework=runtime_framework,
)
managed_agents.append(sub_agent_config)

Expand Down Expand Up @@ -914,7 +967,12 @@ async def create_agent_config(
skills = _get_skills_for_template(agent_id, tenant_id, version_no)

is_manager = len(managed_agents) > 0 or len(external_a2a_agents) > 0
builtin_tools = _get_skill_script_tools(agent_id, tenant_id, version_no)
builtin_tools = _get_skill_script_tools(
agent_id,
tenant_id,
version_no,
allowed_skills=skills,
)
available_tools = tool_list + builtin_tools

_inject_plan_tools(available_tools, enable_planning)
Expand Down Expand Up @@ -1052,6 +1110,8 @@ async def create_agent_config(
agent_config.enable_planning,
any(t.name in {"create_plan", "update_plan_step"} for t in agent_config.tools),
)
agent_config.id = agent_id
agent_config.runtime_framework = runtime_framework
return agent_config


Expand Down Expand Up @@ -1450,6 +1510,73 @@ def check_agent_tools(agent_config: AgentConfig):
return list(used_mcp_urls)


def attach_mcp_bindings(
input_agent_config: AgentConfig,
mcp_info_dict: Dict[str, dict],
) -> None:
"""Attach per-node MCP bindings without re-querying MCP configuration."""
from nexent.core.agents.agent_model import MCPBinding

bindings: list[MCPBinding] = []
tools_by_server: Dict[str, list[ToolConfig]] = {}
for tool in input_agent_config.tools:
if tool.source == "mcp" and tool.usage:
tools_by_server.setdefault(tool.usage, []).append(tool)

for server_name, tools in tools_by_server.items():
record = mcp_info_dict.get(server_name) or {}
url = str(record.get("remote_mcp_server") or "").strip()
headers: Dict[str, str] = {}
auth_token = record.get("authorization_token")
if auth_token:
headers["Authorization"] = str(auth_token)
custom_headers = record.get("custom_headers")
if isinstance(custom_headers, dict):
headers.update({str(key): str(value) for key, value in custom_headers.items()})
tool_names = sorted(
{
str(tool.class_name or tool.name)
for tool in tools
if tool.class_name or tool.name
}
)
required_tool_names = sorted(
{
str(tool.class_name or tool.name)
for tool in tools
if (tool.class_name or tool.name)
and bool((tool.metadata or {}).get("mcp_required", True))
}
)
available = bool(record.get("status") and url)
if not record:
unavailable_reason = "server_not_configured"
elif not record.get("status"):
unavailable_reason = "server_disabled"
elif not url:
unavailable_reason = "server_url_missing"
else:
unavailable_reason = None
bindings.append(
MCPBinding(
server_id=str(record.get("mcp_id") or server_name),
server_name=server_name,
url=url,
transport="sse" if url.endswith("/sse") else "streamable-http",
headers=headers,
required=bool(required_tool_names),
tool_names=tool_names,
required_tool_names=required_tool_names,
available=available,
unavailable_reason=unavailable_reason,
)
)
input_agent_config.mcp_bindings = bindings

for sub_agent_config in input_agent_config.managed_agents:
attach_mcp_bindings(sub_agent_config, mcp_info_dict)


async def create_agent_run_info(
agent_id,
minio_files,
Expand Down Expand Up @@ -1510,10 +1637,18 @@ async def create_agent_run_info(
"status": True,
"authorization_token": None
})
remote_mcp_dict = {record["remote_mcp_server_name"]: record for record in remote_mcp_list if record["status"]}
all_mcp_dict = {record["remote_mcp_server_name"]: record for record in remote_mcp_list}
enabled_mcp_dict = {
name: record
for name, record in all_mcp_dict.items()
if record.get("status")
}
is_agent_config = isinstance(AgentConfig, type) and isinstance(agent_config, AgentConfig)
if is_agent_config:
attach_mcp_bindings(agent_config, all_mcp_dict)

# Filter MCP servers and tools, and build mcp_host with authorization
used_mcp_urls = filter_mcp_servers_and_tools(agent_config, remote_mcp_dict)
used_mcp_urls = filter_mcp_servers_and_tools(agent_config, enabled_mcp_dict)

# Build mcp_host list with authorization tokens and custom headers
mcp_host = []
Expand Down Expand Up @@ -1563,4 +1698,7 @@ async def create_agent_run_info(
),
redis_client=get_redis_client(),
)
if is_agent_config:
agent_run_info.runtime_framework = agent_config.runtime_framework
agent_run_info.mcp_bindings = list(agent_config.mcp_bindings)
return agent_run_info
14 changes: 11 additions & 3 deletions backend/apps/agent_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from consts.const import ASSET_OWNER_TENANT_ID
from consts.model import AgentRequest, AgentInfoRequest, AgentIDRequest, ConversationResponse, AgentImportRequest, AgentNameBatchCheckRequest, AgentNameBatchRegenerateRequest, VersionPublishRequest, VersionListResponse, VersionDetailResponse, VersionRollbackRequest, VersionStatusRequest, CurrentVersionResponse, VersionCompareRequest, VersionUpdateRequest
from consts.exceptions import SkillDuplicateError
from consts.exceptions import AppException, SkillDuplicateError
from services.asset_owner_visibility import apply_agent_detail_prompt_visibility

from services.agent_service import (
Expand Down Expand Up @@ -70,6 +70,8 @@ async def agent_run_api(
authorization=authorization,
resume=resume,
)
except AppException:
raise
except Exception as e:
logger.error(f"Agent run error: {str(e)}")
# Only expose actual error in debug mode for better diagnosis
Expand Down Expand Up @@ -155,6 +157,8 @@ async def update_agent_info_api(request: AgentInfoRequest, authorization: Option
try:
result = await update_agent_info_impl(request, authorization)
return result or {}
except AppException:
raise
except Exception as e:
logger.error(f"Agent update error: {str(e)}")
raise HTTPException(
Expand Down Expand Up @@ -234,6 +238,8 @@ async def import_agent_api(request: AgentImportRequest, authorization: Optional[
force_import=request.force_import
)
return {}
except AppException:
raise
except SkillDuplicateError as exc:
raise HTTPException(status_code=409, detail={
"type": "skill_duplicate",
Expand Down Expand Up @@ -357,6 +363,8 @@ async def publish_version_api(
publish_as_a2a=request.publish_as_a2a,
)
return JSONResponse(status_code=HTTPStatus.OK, content=result)
except AppException:
raise
except ValueError as e:
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
except Exception as e:
Expand Down Expand Up @@ -489,6 +497,8 @@ async def rollback_version_api(
target_version_no=version_no,
)
return JSONResponse(status_code=HTTPStatus.OK, content=result)
except AppException:
raise
except ValueError as e:
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
except Exception as e:
Expand Down Expand Up @@ -627,5 +637,3 @@ async def list_published_agents_api(
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Published agents list error."
)


8 changes: 8 additions & 0 deletions backend/apps/runtime_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@
app.include_router(skill_creator_router)


@app.on_event("shutdown")
async def shutdown_agent_runtimes() -> None:
"""Drain initialized in-process runtimes without importing unused providers."""
from services.agent_runtime.registry import shutdown_initialized_runtimes

await shutdown_initialized_runtimes()


@app.on_event("startup")
async def start_agent_automation_scheduler():
from services.agent_automation.scheduler import agent_automation_scheduler
Expand Down
37 changes: 37 additions & 0 deletions backend/consts/agent_runtime.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Agent-level runtime framework constants and normalization helpers."""

from enum import Enum


class AgentRuntimeFramework(str, Enum):
"""Execution framework persisted with every Agent version."""

SMOLAGENTS = "smolagents"
OPENJIUWEN = "openjiuwen"


DEFAULT_AGENT_RUNTIME_FRAMEWORK = AgentRuntimeFramework.SMOLAGENTS.value
SUPPORTED_AGENT_RUNTIME_FRAMEWORKS = frozenset(item.value for item in AgentRuntimeFramework)


def normalize_agent_runtime_framework(
value: str | AgentRuntimeFramework | None,
*,
default: str | None = DEFAULT_AGENT_RUNTIME_FRAMEWORK,
) -> str | None:
"""Normalize a persisted or request value and reject unsupported frameworks."""
if value is None:
return default
normalized = value.value if isinstance(value, AgentRuntimeFramework) else str(value).strip().lower()
if normalized not in SUPPORTED_AGENT_RUNTIME_FRAMEWORKS:
allowed = ", ".join(sorted(SUPPORTED_AGENT_RUNTIME_FRAMEWORKS))
raise ValueError(f"Unsupported runtime_framework {value!r}; allowed values: {allowed}.")
return normalized


__all__ = [
"AgentRuntimeFramework",
"DEFAULT_AGENT_RUNTIME_FRAMEWORK",
"SUPPORTED_AGENT_RUNTIME_FRAMEWORKS",
"normalize_agent_runtime_framework",
]
6 changes: 6 additions & 0 deletions backend/consts/error_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ class ErrorCode(Enum):
AGENTSPACE_AGENT_RUN_FAILED = "030103" # Agent run failed
AGENTSPACE_AGENT_NAME_DUPLICATE = "030104" # Duplicate agent name
AGENTSPACE_VERSION_NOT_FOUND = "030105" # Agent version not found
AGENT_RUNTIME_FRAMEWORK_IMMUTABLE = "030106" # Runtime framework cannot be changed after creation
AGENT_RUNTIME_FRAMEWORK_MISMATCH = "030107" # Internal parent and child frameworks differ
AGENT_RUNTIME_FRAMEWORK_REQUIRED = "030108" # Blank agent has no selected runtime framework

# ==================== 04 AgentMarket / 智能体市场 ====================
# 01 - Agent
Expand Down Expand Up @@ -245,6 +248,9 @@ class ErrorCode(Enum):
ErrorCode.COMMON_RESOURCE_NOT_FOUND: 404,
ErrorCode.COMMON_RESOURCE_ALREADY_EXISTS: 409,
ErrorCode.COMMON_RESOURCE_DISABLED: 403,
ErrorCode.AGENT_RUNTIME_FRAMEWORK_IMMUTABLE: 409,
ErrorCode.AGENT_RUNTIME_FRAMEWORK_MISMATCH: 409,
ErrorCode.AGENT_RUNTIME_FRAMEWORK_REQUIRED: 409,
# Common - File
ErrorCode.FILE_NOT_FOUND: 404,
ErrorCode.FILE_UPLOAD_FAILED: 500,
Expand Down
Loading
Loading