Skip to content
Merged
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
12 changes: 6 additions & 6 deletions backend/services/a2a_agent_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ def build_a2a_task_response(
text_content = str(message)
task["status"]["message"] = {
"role": message.get("role", "agent"),
"parts": [{"type": "text", "text": text_content, "mediaType": _MEDIA_TYPE_TEXT}]
"parts": [{"text": text_content, "mediaType": _MEDIA_TYPE_TEXT}]
Comment thread
xuyaqist marked this conversation as resolved.
}

# Handle artifacts
Expand Down Expand Up @@ -266,9 +266,9 @@ def build_a2a_message_response(
if parts:
message_parts = parts
elif text:
message_parts = [{"type": "text", "text": text, "mediaType": _MEDIA_TYPE_TEXT}]
message_parts = [{"text": text, "mediaType": _MEDIA_TYPE_TEXT}]
else:
message_parts = [{"type": "text", "text": "", "mediaType": _MEDIA_TYPE_TEXT}]
message_parts = [{"text": "", "mediaType": _MEDIA_TYPE_TEXT}]

message_obj = {
"messageId": message_id,
Expand All @@ -294,8 +294,8 @@ def _content_to_artifact_parts(
return parts
if isinstance(content, dict):
if content.get("type") == "text":
return [{"type": "text", "text": content.get("text", "")}]
return [{"type": "text", "text": str(content)}]
return [{"text": content.get("text", ""), "mediaType": _MEDIA_TYPE_TEXT}]
return [{"text": str(content), "mediaType": _MEDIA_TYPE_TEXT}]

def _map_task_state(self, state: str) -> str:
"""Map shorthand state to TASK_STATE constant."""
Expand Down Expand Up @@ -343,7 +343,7 @@ def _message_to_parts_format(self, message: Any) -> Dict[str, Any]:
text = str(message)
return {
"role": role,
"parts": [{"type": "text", "text": text}]
"parts": [{"text": text}]
}

def _build_artifact_update_event(
Expand Down
4 changes: 2 additions & 2 deletions backend/services/a2a_server_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -647,7 +647,7 @@ async def handle_message_send(
return self.adapter.build_a2a_task_response(
task_id=task_id,
status="TASK_STATE_COMPLETED",
parts=[{"type": "text", "text": accumulated_text, "mediaType": "text/plain"}] if accumulated_text else None,
parts=[{"text": accumulated_text, "mediaType": "text/plain"}] if accumulated_text else None,
Comment thread
xuyaqist marked this conversation as resolved.
context_id=context_id,
Comment thread
xuyaqist marked this conversation as resolved.
timestamp=datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
)
Expand Down Expand Up @@ -879,7 +879,7 @@ def get_task(
message = result.get("message", "")
if message:
task_obj["artifacts"] = [{
"parts": [{"type": "text", "text": str(message)}],
"parts": [{"text": str(message)}],
"lastChunk": True
}]

Expand Down
19 changes: 19 additions & 0 deletions backend/services/user_management_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from utils.auth_utils import (
get_supabase_client,
get_supabase_admin_client,
calculate_expires_at,
get_jwt_expiry_seconds,
)
Expand Down Expand Up @@ -411,6 +412,24 @@
# Get user tenant relationship
user_tenant = get_user_tenant_by_user_id(user_id)
if not user_tenant:
# User exists in Supabase but not in local database - this is an inconsistent state.
# Delete the orphaned Supabase account and return None to trigger 401.
logging.warning(
f"User {user_id} not found in local database, cleaning up orphaned Supabase account"
)
try:
admin_client = get_supabase_admin_client()
if admin_client and hasattr(admin_client.auth, "admin"):
admin_client.auth.admin.delete_user(user_id)
logging.info(f"Deleted orphaned Supabase user {user_id}")
else:
logging.warning(
f"Could not get Supabase admin client to delete user {user_id}"
)
except Exception as delete_err:
logging.error(

Check failure on line 430 in backend/services/user_management_service.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "logging.exception()" instead.

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ44_s-oplKjI8ZEdwCA&open=AZ44_s-oplKjI8ZEdwCA&pullRequest=3002
f"Failed to delete orphaned Supabase user {user_id}: {str(delete_err)}"
)
return None

tenant_id = user_tenant["tenant_id"]
Expand Down
6 changes: 3 additions & 3 deletions doc/docs/en/user-guide/agent-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Nexent supports communication with third-party agents through the A2A protocol.
If you know the Agent Card address of the target agent, you can use the URL discovery method:

<div style="display: flex; justify-content: left;">
<img src="./assets/agent-development/a2a-url-discovery.jpg" style="width: 50%; height: auto;" />
<img src="./assets/agent-development/a2a-url-discovery.jpg" style="width: 80%; height: auto;" />
</div>

1. In the External A2A Agent list, click the "Add External Agent" button
Expand All @@ -72,7 +72,7 @@ If you know the Agent Card address of the target agent, you can use the URL disc
If your agent is registered with the Nacos service discovery platform, you can use the Nacos discovery method:

<div style="display: flex; justify-content: left;">
<img src="./assets/agent-development/a2a-nacos-discovery.jpg" style="width: 50%; height: auto;" />
<img src="./assets/agent-development/a2a-nacos-discovery.jpg" style="width: 80%; height: auto;" />
</div>

1. In the External A2A Agent list, click the "Add External Agent" button
Expand All @@ -94,7 +94,7 @@ If your agent is registered with the Nacos service discovery platform, you can u
In the External A2A Agent list, you can view and manage all discovered external agents:

<div style="display: flex; justify-content: left;">
<img src="./assets/agent-development/a2a-discovery-list.jpg" style="width: 50%; height: auto;" />
<img src="./assets/agent-development/a2a-discovery-list.jpg" style="width: 80%; height: auto;" />
</div>

1. **View Agent Details**: Click on the agent card to view its complete information, including name, description, URL, capability list, etc.
Expand Down
6 changes: 3 additions & 3 deletions doc/docs/zh/user-guide/agent-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Nexent 支持通过 A2A 协议与第三方 Agent 进行通信。您可以通过
如果您知道目标 Agent 的 Agent Card 地址,可以使用 URL 发现方式:

<div style="display: flex; justify-content: left;">
<img src="./assets/agent-development/a2a-url-discovery.jpg" style="width: 50%; height: auto;" />
<img src="./assets/agent-development/a2a-url-discovery.jpg" style="width: 80%; height: auto;" />
</div>

1. 在外部 A2A Agent 列表中,点击"添加外部 Agent"按钮
Expand All @@ -72,7 +72,7 @@ Nexent 支持通过 A2A 协议与第三方 Agent 进行通信。您可以通过
如果您的 Agent 注册在 Nacos 服务发现平台,可以使用 Nacos 发现方式:

<div style="display: flex; justify-content: left;">
<img src="./assets/agent-development/a2a-nacos-discovery.jpg" style="width: 50%; height: auto;" />
<img src="./assets/agent-development/a2a-nacos-discovery.jpg" style="width: 80%; height: auto;" />
</div>

1. 在外部 A2A Agent 列表中,点击"添加外部 Agent"按钮
Expand All @@ -96,7 +96,7 @@ Nexent 支持通过 A2A 协议与第三方 Agent 进行通信。您可以通过


<div style="display: flex; justify-content: left;">
<img src="./assets/agent-development/a2a-discovery-list.jpg" style="width: 50%; height: auto;" />
<img src="./assets/agent-development/a2a-discovery-list.jpg" style="width: 80%; height: auto;" />
</div>

1. **查看 Agent 详情**:点击 Agent 卡片,可以查看其完整信息,包括名称、描述、URL、能力列表等
Expand Down
2 changes: 1 addition & 1 deletion frontend/const/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const languageOptions = [
export const TOKEN_REFRESH_CD = 1 * 60 * 1000;
// If the remaining lifetime of the access token is below this threshold,
// a refresh will be attempted on user activity (sliding expiration).
export const TOKEN_REFRESH_BEFORE_EXPIRY_MS = 5 * 60 * 1000;
export const TOKEN_REFRESH_BEFORE_EXPIRY_MS = 30 * 60 * 1000;
// Throttle interval for activity-driven refresh checks
export const MIN_ACTIVITY_CHECK_INTERVAL_MS = 30 * 1000;

Expand Down
6 changes: 6 additions & 0 deletions frontend/services/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,12 @@ export const fetchWithErrorHandling = async (
throw new ApiError(errorCode, errorMessage);
}

// Handle HTTP 401 - trigger session expired modal for all unauthorized errors
if (response.status === 401) {
handleSessionExpired();
throw new ApiError(errorCode, errorMessage);
}

// Handle custom 499 error code (client closed connection)
if (response.status === 499) {
handleSessionExpired();
Expand Down
8 changes: 3 additions & 5 deletions test/backend/services/test_a2a_agent_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -702,22 +702,22 @@ def test_converts_text_content_dict(self):

content = {"type": "text", "text": "Hello from content"}
result = adapter._content_to_artifact_parts(content, None)
assert result == [{"type": "text", "text": "Hello from content"}]
assert result == [{"text": "Hello from content", "mediaType": "text/plain"}]

def test_converts_non_text_content_to_string(self):
"""Test converts non-dict or non-text content to string."""
adapter = A2AAgentAdapter()

result = adapter._content_to_artifact_parts("Plain string", None)
assert result == [{"type": "text", "text": "Plain string"}]
assert result == [{"text": "Plain string", "mediaType": "text/plain"}]

def test_converts_non_text_dict_to_string(self):
"""Test converts dict content without text type to string."""
adapter = A2AAgentAdapter()

content = {"type": "image", "data": "base64..."}
result = adapter._content_to_artifact_parts(content, None)
assert result == [{"type": "text", "text": str(content)}]
assert result == [{"text": str(content), "mediaType": "text/plain"}]


class TestMessageToPartsFormat:
Expand All @@ -744,7 +744,6 @@ def test_converts_message_with_text_content(self):
}
result = adapter._message_to_parts_format(message)
assert result["role"] == "user"
assert result["parts"][0]["type"] == "text"
assert result["parts"][0]["text"] == "User message content"

def test_converts_message_with_non_text_content(self):
Expand Down Expand Up @@ -804,7 +803,6 @@ def test_response_with_text_content_dict(self):
)

assert result["task"]["status"]["message"]["role"] == "agent"
assert result["task"]["status"]["message"]["parts"][0]["type"] == "text"
assert result["task"]["status"]["message"]["parts"][0]["text"] == "Agent response text"
assert result["task"]["status"]["message"]["parts"][0]["mediaType"] == "text/plain"

Expand Down
14 changes: 10 additions & 4 deletions test/backend/services/test_user_management_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -1313,18 +1313,24 @@ async def test_get_user_info_success(self, mock_query_group_ids, mock_get_user_t
{"permission_type": "LEFT_NAV_MENU", "permission_subtype": "chat"}
])

@patch('backend.services.user_management_service.get_supabase_admin_client')
@patch('backend.services.user_management_service.get_user_tenant_by_user_id')
async def test_get_user_info_user_not_found(self, mock_get_user_tenant):
"""Test getting user information when user doesn't exist"""
async def test_get_user_info_user_not_found(self, mock_get_user_tenant, mock_get_admin_client):
"""Test getting user information when user doesn't exist - orphan cleanup is triggered"""
# Setup mocks
mock_get_user_tenant.return_value = None
mock_admin_client = MagicMock()
mock_admin_client.auth.admin.delete_user = MagicMock()
mock_get_admin_client.return_value = mock_admin_client

# Execute
result = await get_user_info("nonexistent_user")
result = await get_user_info("orphan_user")

# Assert
assert result is None
mock_get_user_tenant.assert_called_once_with("nonexistent_user")
mock_get_user_tenant.assert_called_once_with("orphan_user")
mock_get_admin_client.assert_called_once()
mock_admin_client.auth.admin.delete_user.assert_called_once_with("orphan_user")

@patch('backend.services.user_management_service.get_user_tenant_by_user_id')
@patch('backend.services.user_management_service.query_group_ids_by_user')
Expand Down
Loading