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
18 changes: 17 additions & 1 deletion backend/apps/agent_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
list_all_agent_info_impl,
run_agent_stream,
stop_agent_tasks,
get_agent_call_relationship_impl
get_agent_call_relationship_impl,
clear_agent_new_mark_impl
)
from utils.auth_utils import get_current_user_info, get_current_user_id

Expand Down Expand Up @@ -148,6 +149,21 @@ async def import_agent_api(request: AgentImportRequest, authorization: Optional[
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Agent import error.")


@agent_config_router.put("/clear_new/{agent_id}")
async def clear_agent_new_mark_api(agent_id: int, authorization: Optional[str] = Header(None)):
"""
Clear the NEW mark for an agent
"""
try:
user_id, tenant_id, _ = get_current_user_info(authorization)
affected_rows = await clear_agent_new_mark_impl(agent_id, tenant_id, user_id)
return {"message": "Agent NEW mark cleared successfully", "affected_rows": affected_rows}
except Exception as e:
logger.error(f"Failed to clear agent NEW mark: {str(e)}")
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Failed to clear agent NEW mark.")


@agent_config_router.post("/check_name")
async def check_agent_name_batch_api(request: AgentNameBatchCheckRequest, authorization: Optional[str] = Header(None)):
"""
Expand Down
44 changes: 44 additions & 0 deletions backend/database/agent_db.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import logging
from typing import List

from sqlalchemy import update
from database.client import get_db_session, as_dict, filter_property
from database.db_models import AgentInfo, ToolInstance, AgentRelation
from sqlalchemy import update, bindparam
Comment thread
geruihappy-creator marked this conversation as resolved.

logger = logging.getLogger("agent_db")

Expand Down Expand Up @@ -68,6 +70,47 @@ def query_sub_agents_id_list(main_agent_id: int, tenant_id: str):
return [relation.selected_agent_id for relation in relations]


def clear_agent_new_mark(agent_id: int, tenant_id: str, user_id: str):
"""
Clear the NEW mark for an agent

Args:
agent_id (int): Agent ID
tenant_id (str): Tenant ID
user_id (str): User ID (for audit purposes)
"""
with get_db_session() as session:
result = session.execute(
update(AgentInfo)
.where(
AgentInfo.agent_id == agent_id,
AgentInfo.tenant_id == tenant_id,
AgentInfo.delete_flag == 'N'
)
.values(is_new=False, updated_by=user_id)
)
# return number of rows affected
return result.rowcount


def mark_agents_as_new(agent_ids: list[int], tenant_id: str, user_id: str):
"""
Mark a list of agents as new (is_new = True)
"""
if not agent_ids:
return
with get_db_session() as session:
session.execute(
update(AgentInfo)
.where(
AgentInfo.agent_id.in_(agent_ids),
AgentInfo.tenant_id == tenant_id,
AgentInfo.delete_flag == 'N'
)
.values(is_new=True, updated_by=user_id)
)


def create_agent(agent_info, tenant_id: str, user_id: str):
"""
Create a new agent in the database.
Expand All @@ -82,6 +125,7 @@ def create_agent(agent_info, tenant_id: str, user_id: str):
"tenant_id": tenant_id,
"created_by": user_id,
"updated_by": user_id,
"is_new": True, # Mark new agents as new
})
with get_db_session() as session:
new_agent = AgentInfo(**filter_property(info_with_metadata, AgentInfo))
Expand Down
1 change: 1 addition & 0 deletions backend/database/db_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ class AgentInfo(TableBase):
business_logic_model_name = Column(String(100), doc="Model name used for business logic prompt generation")
business_logic_model_id = Column(Integer, doc="Model ID used for business logic prompt generation, foreign key reference to model_record_t.model_id")
group_ids = Column(String, doc="Agent group IDs list")
is_new = Column(Boolean, default=False, doc="Whether this agent is marked as new for the user")


class ToolInstance(TableBase):
Expand Down
23 changes: 22 additions & 1 deletion backend/services/agent_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
search_agent_info_by_agent_id,
search_blank_sub_agent_by_main_agent_id,
update_agent,
update_related_agents
update_related_agents,
clear_agent_new_mark
)
from database.model_management_db import get_model_by_model_id, get_model_id_by_display_name
from database.remote_mcp_db import get_mcp_server_by_name_and_tenant
Expand Down Expand Up @@ -1114,6 +1115,9 @@ async def import_agent_impl(
agent_stack.append(need_import_agent_id)
agent_stack.extend(managed_agents)

# Return the mapping of original IDs to new IDs
return mapping_agent_id


async def import_agent_by_agent_id(
import_agent_info: ExportAndImportAgentInfo,
Expand Down Expand Up @@ -1221,6 +1225,22 @@ def load_default_agents_json_file(default_agent_path):
return all_json_files


async def clear_agent_new_mark_impl(agent_id: int, tenant_id: str, user_id: str):
"""
Clear the NEW mark for an agent

Args:
agent_id (int): Agent ID
tenant_id (str): Tenant ID
user_id (str): User ID (for audit purposes)
"""
rowcount = clear_agent_new_mark(agent_id, tenant_id, user_id)
logger.info(f"clear_agent_new_mark_impl called for agent_id={agent_id}, tenant_id={tenant_id}, user_id={user_id}, affected_rows={rowcount}")
return rowcount




async def list_all_agent_info_impl(tenant_id: str) -> list[dict]:
"""
list all agent info
Expand Down Expand Up @@ -1275,6 +1295,7 @@ async def list_all_agent_info_impl(tenant_id: str) -> list[dict]:
"author": agent.get("author"),
"is_available": len(unavailable_reasons) == 0,
"unavailable_reasons": unavailable_reasons,
"is_new": agent.get("is_new", False),
"group_ids": convert_string_to_list(agent.get("group_ids"))
})

Expand Down
18 changes: 17 additions & 1 deletion docker/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ CREATE TABLE IF NOT EXISTS nexent.ag_tenant_agent_t (
tenant_id VARCHAR(100),
group_ids VARCHAR,
enabled BOOLEAN DEFAULT FALSE,
is_new BOOLEAN DEFAULT FALSE,
provide_run_summary BOOLEAN DEFAULT FALSE,
create_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
Expand Down Expand Up @@ -364,6 +365,12 @@ COMMENT ON COLUMN nexent.ag_tenant_agent_t.update_time IS 'Update time';
COMMENT ON COLUMN nexent.ag_tenant_agent_t.created_by IS 'Creator';
COMMENT ON COLUMN nexent.ag_tenant_agent_t.updated_by IS 'Updater';
COMMENT ON COLUMN nexent.ag_tenant_agent_t.delete_flag IS 'Whether it is deleted. Optional values: Y/N';
COMMENT ON COLUMN nexent.ag_tenant_agent_t.is_new IS 'Whether this agent is marked as new for the user';

-- Create index for is_new queries
CREATE INDEX IF NOT EXISTS idx_ag_tenant_agent_t_is_new
ON nexent.ag_tenant_agent_t (tenant_id, is_new)
WHERE delete_flag = 'N';


-- Create the ag_tool_instance_t table in the nexent schema
Expand Down Expand Up @@ -522,6 +529,8 @@ CREATE TABLE IF NOT EXISTS nexent.user_tenant_t (
user_tenant_id SERIAL PRIMARY KEY,
user_id VARCHAR(100) NOT NULL,
tenant_id VARCHAR(100) NOT NULL,
user_role VARCHAR(30) DEFAULT 'USER',
user_email VARCHAR(255),
create_time TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW(),
update_time TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW(),
created_by VARCHAR(100),
Expand All @@ -535,6 +544,8 @@ COMMENT ON TABLE nexent.user_tenant_t IS 'User tenant relationship table';
COMMENT ON COLUMN nexent.user_tenant_t.user_tenant_id IS 'User tenant relationship ID, primary key';
COMMENT ON COLUMN nexent.user_tenant_t.user_id IS 'User ID';
COMMENT ON COLUMN nexent.user_tenant_t.tenant_id IS 'Tenant ID';
COMMENT ON COLUMN nexent.user_tenant_t.user_role IS 'User role: SUPER_ADMIN, ADMIN, DEV, USER';
COMMENT ON COLUMN nexent.user_tenant_t.user_email IS 'User email address';
COMMENT ON COLUMN nexent.user_tenant_t.create_time IS 'Create time';
COMMENT ON COLUMN nexent.user_tenant_t.update_time IS 'Update time';
COMMENT ON COLUMN nexent.user_tenant_t.created_by IS 'Created by';
Expand Down Expand Up @@ -1011,4 +1022,9 @@ INSERT INTO nexent.role_permission_t (role_permission_id, user_role, permission_
(208, 'SPEED', 'RESOURCE', 'GROUP', 'READ'),
(209, 'SPEED', 'RESOURCE', 'GROUP', 'UPDATE'),
(210, 'SPEED', 'RESOURCE', 'GROUP', 'DELETE')
ON CONFLICT (role_permission_id) DO NOTHING;
ON CONFLICT (role_permission_id) DO NOTHING;

-- Insert SPEED role user into user_tenant_t table if not exists
INSERT INTO nexent.user_tenant_t (user_id, tenant_id, user_role, user_email, created_by, updated_by)
VALUES ('user_id', 'tenant_id', 'SPEED', NULL, 'system', 'system')
ON CONFLICT (user_id, tenant_id) DO NOTHING;
16 changes: 16 additions & 0 deletions docker/sql/v1.7.9.3_0122_add_is_new_to_ag_tenant_agent_t.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- Add is_new column to ag_tenant_agent_t table for new agent marking
-- This migration adds a field to track whether an agent is marked as new for users

-- Add is_new column with default value false
ALTER TABLE nexent.ag_tenant_agent_t
ADD COLUMN IF NOT EXISTS is_new BOOLEAN DEFAULT FALSE;

-- Add comment for the new column
COMMENT ON COLUMN nexent.ag_tenant_agent_t.is_new IS 'Whether this agent is marked as new for the user';

-- Create index for performance on is_new queries
CREATE INDEX IF NOT EXISTS idx_ag_tenant_agent_t_is_new
ON nexent.ag_tenant_agent_t (tenant_id, is_new)
WHERE delete_flag = 'N';
Comment thread
geruihappy-creator marked this conversation as resolved.


12 changes: 12 additions & 0 deletions frontend/app/[locale]/agents/components/AgentManageComp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import log from "@/lib/logger";
import { useState, useEffect } from "react";
import { ImportAgentData } from "@/hooks/useAgentImport";
import AgentImportWizard from "@/components/agent/AgentImportWizard";
import { clearAgentNewMark } from "@/services/agentConfigService";
import { clearAgentAndSync } from "@/lib/agentNewUtils";

export default function AgentManageComp() {
const { t } = useTranslation("common");
Expand Down Expand Up @@ -156,6 +158,16 @@ export default function AgentManageComp() {
return;
}

// Clear NEW mark when agent is selected for editing
try {
const res = await clearAgentAndSync(agent.id, queryClient);
if (!res?.success) {
log.warn("Failed to clear NEW mark on select:", res);
}
} catch (err) {
log.error("Failed to clear NEW mark on select:", err);
}

// Set selected agent id to trigger the hook
setSelectedAgentId(Number(agent.id));
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Button, Col, Flex, Tooltip, Divider, Table, theme, App } from "antd";
import { ExclamationCircleOutlined } from "@ant-design/icons";
Expand All @@ -17,7 +17,9 @@ import {
exportAgent,
updateToolConfig,
} from "@/services/agentConfigService";
import { clearAgentNewMark } from "@/services/agentConfigService";
import log from "@/lib/logger";
import { clearAgentAndSync } from "@/lib/agentNewUtils";

interface AgentListProps {
agentList: Agent[];
Expand All @@ -40,6 +42,27 @@ export default function AgentList({
const confirm = useConfirmModal();
const queryClient = useQueryClient();

// Note: rely on agent.is_new from agentList (single source of truth).
// Clear NEW mark when agent is selected (sync with selection visual feedback)
useEffect(() => {
if (currentAgentId) {
const agentId = String(currentAgentId);
const agent = agentList.find(a => String(a.id) === agentId);
if (agent?.is_new) {
(async () => {
try {
const res = await clearAgentAndSync(agentId, queryClient);
Comment thread
geruihappy-creator marked this conversation as resolved.
if (!res?.success) {
log.warn("Failed to clear NEW mark for agent:", agentId, res);
}
} catch (err) {
log.error("Error clearing NEW mark:", err);
}
})();
}
}
}, [currentAgentId, agentList]);

// Call relationship modal state
const [callRelationshipModalVisible, setCallRelationshipModalVisible] =
useState(false);
Expand Down Expand Up @@ -279,6 +302,8 @@ export default function AgentList({
onClick: (e: any) => {
e.preventDefault();
e.stopPropagation();

// Call onSelectAgent - NEW mark clearing is handled by useEffect
onSelectAgent(agent);
},
})}
Expand All @@ -292,6 +317,7 @@ export default function AgentList({
const isSelected =
currentAgentId !== null &&
String(currentAgentId) === String(agent.id);
const isNew = agent.is_new || false;

return (
<Flex
Expand Down Expand Up @@ -332,6 +358,13 @@ export default function AgentList({
<ExclamationCircleOutlined className="text-amber-500 text-sm flex-shrink-0 cursor-pointer" />
</Tooltip>
)}
{isNew && (
<Tooltip title={t("space.new", "New imported agent")}>
<span className="inline-flex items-center px-1 h-5 bg-amber-50 dark:bg-amber-900/10 text-amber-700 dark:text-amber-300 rounded-full text-[11px] font-medium border border-amber-200 flex-shrink-0 leading-none">
<span className="px-0.5">{t("space.new", "NEW")}</span>
</span>
</Tooltip>
)}
{displayName && (
<span className="text-base leading-normal max-w-[220px] truncate break-all">
{displayName}
Expand Down
31 changes: 29 additions & 2 deletions frontend/app/[locale]/chat/components/chatAgentSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import { fetchAllAgents } from "@/services/agentConfigService";
import { getUrlParam } from "@/lib/utils";
import log from "@/lib/logger";
import { Agent, ChatAgentSelectorProps } from "@/types/chat";
import { clearAgentNewMark } from "@/services/agentConfigService";
import { useQueryClient } from "@tanstack/react-query";
import { clearAgentAndSync } from "@/lib/agentNewUtils";

export function ChatAgentSelector({
selectedAgentId,
Expand All @@ -34,6 +37,7 @@ export function ChatAgentSelector({
const selectedAgent = agents.find(
(agent) => agent.agent_id === selectedAgentId
);
const queryClient = useQueryClient();

// Detect duplicate agent names and mark later-added agents as disabled
// For agents with the same name, keep the first one (smallest ID) enabled, disable the rest
Expand Down Expand Up @@ -198,18 +202,35 @@ export function ChatAgentSelector({
}
};

const handleAgentSelect = (agentId: number | null) => {
const handleAgentSelect = async (agentId: number | null) => {
// Only effectively available agents can be selected
if (agentId !== null) {
const agent = agents.find((a) => a.agent_id === agentId);
if (agent) {
const isAvailableTool = agent.is_available !== false;
const isDuplicateDisabled = duplicateAgentInfo.disabledAgentIds.has(agent.agent_id);
const isEffectivelyAvailable = isAvailableTool && !isDuplicateDisabled;

if (!isEffectivelyAvailable) {
return; // Unavailable agents cannot be selected
}

// Clear NEW mark when agent is selected for chat
try {
const res = await clearAgentAndSync(agentId, queryClient);
Comment thread
geruihappy-creator marked this conversation as resolved.
if (res?.success) {
// update local agents state to reflect cleared NEW mark immediately
setAgents((prev) =>
prev.map((a) =>
a.agent_id === agentId ? { ...a, is_new: false } : a
)
);
} else {
log.warn("Failed to clear NEW mark on select:", res);
}
} catch (e) {
log.error("Failed to clear NEW mark on select:", e);
}
}
}

Expand Down Expand Up @@ -417,6 +438,12 @@ export function ChatAgentSelector({
}`}
>
<div className="flex items-center gap-1.5">
{/* NEW badge - placed before display_name */}
{(agent as any).is_new && agent.display_name && (
<span className="inline-flex items-center px-1 h-5 bg-amber-50 dark:bg-amber-900/10 text-amber-700 dark:text-amber-300 rounded-full text-[11px] font-medium border border-amber-200 flex-shrink-0 leading-none mr-0.5">
<span className="px-0.5">{t("space.new", "NEW")}</span>
</span>
)}
{agent.display_name && (
<span className="text-sm leading-none">
{agent.display_name}
Expand Down
Loading
Loading