Skip to content
Merged

v2.8.3 #4272

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
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,7 @@ For a reusable component with non-obvious behavior, document:
- related components, hooks, services, or translation namespaces.

Keep the document focused on decisions that an implementer or reviewer could otherwise miss. Update it when the component's contract changes.

## Shared resource descriptions

ResourceItem owns two-line description truncation and full-content Ant Design Tooltip behavior for tools, skills, knowledge and Workbench compatibility rows. Consumers must not add nested description tooltips for individual resource sources. Preserve renderDescription markup; use the full item description for tooltip content, falling back to the rendered description when no plain description exists. Empty descriptions produce no empty tooltip. Hovering never selects or persists a resource; action controls remain separate. MCP service headers use their existing standalone tooltip because they are not ResourceItem rows.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# MCP tool group

ToolDrawer owns filtered service groups, service pagination, eligible tool IDs, local draft selection and batch parameter loading. McpToolGroup owns expansion only and renders existing ResourceItem children through a callback. Expanding never selects tools; the independent labelled checkbox reflects eligible or already-selected matching children, including mixed state. Batch loading disables group/page checkboxes and confirmation. Required parameters remain configurable individually; batch selection skips tools requiring configuration and duplicate names. Failed lookups leave the batch unchanged and allow retry. Closing invalidates pending batch work.

Search matches service metadata and tool metadata, while tag predicates still intersect tool IDs. A group action affects matching children; the MCP header selects the current service page. Other sources keep flat pagination/selection. Missing service metadata uses usage as the name; no visibility rule is relaxed. Service description and tags come from the existing MCP list endpoint. UI labels use toolPool translations. Checkbox and expansion controls are separate keyboard targets with aria-checked/aria-expanded/aria-controls. The optional ResourceDrawer selectAllLabel preserves the default label for existing callers.
94 changes: 68 additions & 26 deletions .github/workflows/auto-unit-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,6 @@ concurrency:
on:
workflow_dispatch:
inputs:
runner_label_json:
description: 'JSON runner labels; blank uses self-hosted, or use ["ubuntu-24.04"] for GitHub-hosted'
required: false
default: ''
pytest_workers:
description: 'parallel test workers (auto=CPU count, 0=serial, N=fixed count)'
required: false
default: 'auto'
pytest_file_timeout:
description: 'per-file timeout in seconds (0=disabled)'
required: false
Expand Down Expand Up @@ -53,38 +45,40 @@ on:

jobs:
test:
runs-on: ${{ fromJson(inputs.runner_label_json || '["self-hosted", "linux", "x64"]') }}
runs-on: ut
env:
UV_PYTHON: "3.11.15"
CODECOV_CLI_VERSION: "11.3.2"
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Run deployment script tests
- name: Install uv
# astral-sh/setup-uv@v10.3.3
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2
with:
version: "latest"

- name: Set up managed Python
run: |
bash deploy/tests/test_common.sh
bash deploy/tests/test_docker_backup.sh
bash deploy/tests/test_k8s_backup.sh
bash deploy/tests/test_sql_migrations.sh
bash deploy/tests/test_super_admin_init.sh
bash deploy/tests/test_build_offline_package.sh
uv python install 3.11.15 --no-bin
managed_python_bin="$(dirname "$(uv python find --managed-python 3.11.15)")"
echo "$managed_python_bin" >> "$GITHUB_PATH"

- name: Set up Python
uses: actions/setup-python@v4
- name: Install Helm test dependency
uses: azure/setup-helm@v4
with:
python-version: '3.11'

- name: Install uv
run: pip install --upgrade uv
version: "v3.21.3"

- name: Install dependencies
run: |
cd backend
uv sync --extra data-process --extra test
uv pip install -e "../sdk[dev]"
uv sync --managed-python --extra data-process --extra test --index pytorch-cpu --upgrade-package torch --upgrade-package torchvision
cd ..

- name: Run all tests and collect coverage
# Configure NEXENT_PYTEST_WORKERS in the runner environment; the default is the CPU count.
env:
NEXENT_PYTEST_WORKERS: ${{ github.event.inputs.pytest_workers || 'auto' }}
NEXENT_PYTEST_FILE_TIMEOUT: ${{ github.event.inputs.pytest_file_timeout || '300' }}
run: |
source backend/.venv/bin/activate && python test/run_all_test.py
Expand All @@ -104,14 +98,62 @@ jobs:
echo "All tests passed successfully."
fi

- name: Set up cached Codecov CLI
id: codecov-cli
env:
CODECOV_OS: ${{ inputs.codecov_os || (runner.arch == 'ARM64' && 'linux-arm64' || 'linux') }}
run: |
set -euo pipefail
# Pin release digests so cache hits are verified without network requests.
case "$CODECOV_OS" in
linux)
asset=codecovcli_linux
checksum=8e1bfb71661378530f6ea21e82b414095899e565cbb4cf555c613064be17b973
;;
linux-arm64)
asset=codecovcli_linux_arm64
checksum=a4b71cac685742e272e7cfd4a390cafc3736f57ebc3cf7ba2e24010f19c7f23f
;;
*) echo "Unsupported Codecov OS: $CODECOV_OS" >&2; exit 1 ;;
esac
# Prefer runner configuration, then the standard Unix cache location.
cache_dir="${CODECOV_CACHE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/codecov}"
mkdir -p "$cache_dir"
cache_dir="$(cd "$cache_dir" && pwd)"
binary="$cache_dir/codecov"
verify_binary() {
printf '%s %s\n' "$checksum" "$1" | sha256sum --check --status
}
if [ -f "$binary" ] && verify_binary "$binary"; then
echo "Using cached Codecov CLI $CODECOV_CLI_VERSION ($CODECOV_OS)"
else
temporary_binary="$(mktemp "$cache_dir/codecov.XXXXXX")"
trap 'rm -f "$temporary_binary"' EXIT
curl --fail --silent --show-error --location --retry 3 \
--connect-timeout 15 --max-time 180 \
"https://github.com/codecov/codecov-cli/releases/download/v$CODECOV_CLI_VERSION/$asset" \
--output "$temporary_binary"
if ! verify_binary "$temporary_binary"; then
echo "Codecov CLI checksum verification failed" >&2
exit 1
fi
chmod +x "$temporary_binary"
mv -f "$temporary_binary" "$binary"
fi
if [ ! -x "$binary" ]; then chmod +x "$binary"; fi
echo "binary=$binary" >> "$GITHUB_OUTPUT"

- name: Upload coverage to Codecov
# codecov/codecov-action@v7
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f
with:
binary: ${{ steps.codecov-cli.outputs.binary }}
version: ${{ env.CODECOV_CLI_VERSION }}
files: test/coverage.xml
disable_search: true
token: ${{ secrets.CODECOV_TOKEN }}
flags: unittests
name: codecov-umbrella
fail_ci_if_error: true
os: ${{ inputs.codecov_os || (runner.arch == 'ARM64' && 'linux-arm64' || 'linux') }}
verbose: true
verbose: false
4 changes: 2 additions & 2 deletions .github/workflows/nexent-daily-full-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ concurrency:
jobs:
daily-full-test:
name: Daily D0-D6
runs-on: [self-hosted, linux, x64]
runs-on: [e2e]
# Includes up to 12 hours waiting for the local Daily lock.
timeout-minutes: 1440
defaults:
run:
shell: bash
env:
TEST_ROOT: /home/jason/nexent-test-suite
REPO_ROOT: /root/project/nexent
REPO_ROOT: /home/jason/projects/nexent
TEST_PYTHON: /home/jason/nexent-test-suite/runtime/test-venv/bin/python

steps:
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/sdk_publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ jobs:
with:
python-version: '3.11'

- name: Install build dependencies
run: |
python -m pip install --upgrade pip
pip install build
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
version: "0.13.6"

- name: Update version if specified
if: ${{ inputs.version != '' }}
Expand All @@ -36,7 +36,7 @@ jobs:

- name: Build package
working-directory: sdk
run: python -m build
run: uv build --python python

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v2.8.2
v2.8.3
3 changes: 2 additions & 1 deletion backend/agents/create_agent_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
from infrastructure.redis import get_redis_client
from consts.const import (
AGENT_WORKSPACE_ROOT,
A2A_AGENT_INVOCATION_TIMEOUT_SECONDS,
NEXENT_SANDBOX_WORKSPACE_MODE,
NEXENT_SANDBOX_CONTAINER_WORKSPACE_ROOT,
NEXENT_SANDBOX_FAILURE_POLICY,
Expand Down Expand Up @@ -986,7 +987,7 @@ def _build_external_agent_config(agent: dict, agent_url: str) -> ExternalA2AAgen
transport_type=agent.get("transport_type", "http-streaming"),
protocol_version=agent.get("protocol_version", "1.0"),
protocol_type=agent.get("protocol_type", PROTOCOL_JSONRPC),
timeout=300.0,
timeout=A2A_AGENT_INVOCATION_TIMEOUT_SECONDS,
ssl_verify=agent.get("ssl_verify", True),
raw_card=agent.get("raw_card"),
custom_headers=_build_security_headers(agent) or None,
Expand Down
4 changes: 3 additions & 1 deletion backend/apps/agent_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import logging
from http import HTTPStatus
from urllib.parse import quote
from typing import Optional
from typing import Literal, Optional

from fastapi import APIRouter, Body, Depends, File, Header, HTTPException, Query, Request, UploadFile
from fastapi.encoders import jsonable_encoder
Expand Down Expand Up @@ -299,6 +299,7 @@ async def agent_run_api(
async def northbound_agent_run_api(
agent_request: AgentRequest,
authorization: Optional[str] = Header(None),
final_answer_mode: Literal["complete", "delta"] = Query("complete"),
):
"""Run a northbound-prepared agent request inside the runtime service."""
try:
Expand All @@ -310,6 +311,7 @@ async def northbound_agent_run_api(
user_id=user_id,
tenant_id=tenant_id,
skip_user_save=True,
final_answer_mode=final_answer_mode,
)
except UnauthorizedError as exc:
raise HTTPException(
Expand Down
9 changes: 8 additions & 1 deletion backend/apps/northbound_app.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Copyright (c) 2025-2026 Huawei Technologies Co., Ltd. All rights reserved.
import logging
from http import HTTPStatus
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Literal, Optional
from urllib.parse import urlparse, unquote
import re
import uuid
Expand Down Expand Up @@ -445,6 +445,12 @@ async def run_chat(
}
}],
),
final_answer_mode: Literal["complete", "delta"] = Body(
"complete",
embed=True,
description="Final answer delivery: complete returns one full root answer (default); "
"delta preserves incremental final_answer events and their completion phase.",
),
idempotency_key: Optional[str] = Header(None, alias="Idempotency-Key"),
):
try:
Expand All @@ -461,6 +467,7 @@ async def run_chat(
tool_params=tool_params,
model_id=model_id,
idempotency_key=idempotency_key,
final_answer_mode=final_answer_mode,
)
except LimitExceededError as e:
logging.error(f"Too Many Requests: rate limit exceeded: {str(e)}", exc_info=e)
Expand Down
5 changes: 5 additions & 0 deletions backend/consts/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ class VectorDatabaseType(str, Enum):
AGENT_AUTOMATION_MIN_INTERVAL_SECONDS = int(
os.getenv("AGENT_AUTOMATION_MIN_INTERVAL_SECONDS", "5")
)
# Timeout in seconds for invoking external A2A agents (both the runtime
# sub-agent path and the northbound direct-call path)
A2A_AGENT_INVOCATION_TIMEOUT_SECONDS = float(
os.getenv("A2A_AGENT_INVOCATION_TIMEOUT_SECONDS", "900")
)


# Container-internal skills storage path
Expand Down
40 changes: 40 additions & 0 deletions backend/database/conversation_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,46 @@ class MessageRecord(TypedDict):
opinion_flag: Optional[str]


def load_execution_result_content(
user_id: str, tenant_id: str, conversation_id: int, message_id: int, unit_index: int,
) -> str:
"""Load one execution unit in the caller's current conversation scope."""
from .db_models import UserTenant

membership = select(UserTenant.user_id).where(
UserTenant.user_id == user_id,
UserTenant.tenant_id == tenant_id,
UserTenant.delete_flag == "N",
).exists()
statement = (
select(ConversationMessageUnit.unit_content)
.join(ConversationMessage, ConversationMessage.message_id == ConversationMessageUnit.message_id)
.join(ConversationRecord, ConversationRecord.conversation_id == ConversationMessage.conversation_id)
.where(
membership,
ConversationRecord.created_by == user_id,
ConversationRecord.conversation_id == conversation_id,
ConversationRecord.delete_flag == "N",
ConversationMessage.message_id == message_id,
ConversationMessage.delete_flag == "N",
ConversationMessageUnit.conversation_id == conversation_id,
ConversationMessageUnit.unit_index == unit_index,
ConversationMessageUnit.unit_type == "execution_result",
ConversationMessageUnit.delete_flag == "N",
)
)
with get_db_session() as session:
rows = session.execute(statement).scalars().all()
if not rows:
raise PermissionError("Execution result is unavailable or not authorized")
if len(rows) != 1:
raise ValueError("Ambiguous execution result reference")
payload = json.loads(rows[0])
if not isinstance(payload, dict) or not isinstance(payload.get("content"), str):
raise ValueError("Invalid persisted execution content")
return payload["content"]


def _serialize_unit_content(content: Any) -> str:
"""Serialize structured unit content for the text database column."""
if isinstance(content, str):
Expand Down
7 changes: 5 additions & 2 deletions backend/infrastructure/http/a2a.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@
from typing import Any, AsyncIterator, Dict, Optional
import aiohttp

from consts.const import A2A_AGENT_INVOCATION_TIMEOUT_SECONDS

logger = logging.getLogger(__name__)

# Default timeout for A2A requests (5 minutes)
DEFAULT_TIMEOUT = 300.0
# Default timeout for A2A requests, driven by A2A_AGENT_INVOCATION_TIMEOUT_SECONDS
# (defaults to 15 minutes)
DEFAULT_TIMEOUT = A2A_AGENT_INVOCATION_TIMEOUT_SECONDS
# Shorter timeout for lightweight requests like agent-card (10 seconds)
AGENT_CARD_TIMEOUT = 10.0
# Retry settings
Expand Down
Loading
Loading