Skip to content

Commit 12b1b7b

Browse files
authored
Merge branch 'release/v1.7.4' into xyc/batch_model_improve
2 parents 8047bbe + f679356 commit 12b1b7b

File tree

95 files changed

+3656
-1068
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

95 files changed

+3656
-1068
lines changed

backend/agents/create_agent_info.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from services.memory_config_service import build_memory_context
1616
from database.agent_db import search_agent_info_by_agent_id, query_sub_agents_id_list
1717
from database.tool_db import search_tools_for_sub_agent
18-
from database.model_management_db import get_model_records
18+
from database.model_management_db import get_model_records, get_model_by_model_id
1919
from utils.model_name_utils import add_repo_to_name
2020
from utils.prompt_template_utils import get_agent_prompt_template
2121
from utils.config_utils import tenant_config_manager, get_model_name_from_config
@@ -170,6 +170,11 @@ async def create_agent_config(
170170
else:
171171
system_prompt = agent_info.get("prompt", "")
172172

173+
if agent_info.get("model_id") is not None:
174+
model_info = get_model_by_model_id(agent_info.get("model_id"))
175+
model_name = model_info["display_name"] if model_info is not None else "main_model"
176+
else:
177+
model_name = "main_model"
173178
agent_config = AgentConfig(
174179
name="undefined" if agent_info["name"] is None else agent_info["name"],
175180
description="undefined" if agent_info["description"] is None else agent_info["description"],
@@ -180,7 +185,7 @@ async def create_agent_config(
180185
),
181186
tools=tool_list,
182187
max_steps=agent_info.get("max_steps", 10),
183-
model_name=agent_info.get("model_name"),
188+
model_name=model_name,
184189
provide_run_summary=agent_info.get("provide_run_summary", False),
185190
managed_agents=managed_agents
186191
)

backend/apps/tenant_config_app.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from fastapi import APIRouter, Body, Header, HTTPException
66
from fastapi.responses import JSONResponse
77

8-
from consts.const import DEPLOYMENT_VERSION
8+
from consts.const import DEPLOYMENT_VERSION, APP_VERSION
99
from services.tenant_config_service import get_selected_knowledge_list, update_selected_knowledge
1010
from utils.auth_utils import get_current_user_id
1111

@@ -22,6 +22,7 @@ def get_deployment_version():
2222
return JSONResponse(
2323
status_code=HTTPStatus.OK,
2424
content={"deployment_version": DEPLOYMENT_VERSION,
25+
"app_version": APP_VERSION,
2526
"status": "success"}
2627
)
2728
except Exception as e:

backend/apps/tool_config_app.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
from services.tool_configuration_service import (
1111
search_tool_info_impl,
1212
update_tool_info_impl,
13-
update_tool_list, list_all_tools,
13+
update_tool_list,
14+
list_all_tools,
15+
load_last_tool_config_impl,
1416
)
1517
from utils.auth_utils import get_current_user_id
1618

@@ -78,3 +80,21 @@ async def scan_and_update_tool(
7880
logger.error(f"Failed to update tool: {e}")
7981
raise HTTPException(
8082
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Failed to update tool")
83+
84+
@router.get("/load_config/{tool_id}")
85+
async def load_last_tool_config(tool_id: int, authorization: Optional[str] = Header(None)):
86+
try:
87+
user_id, tenant_id = get_current_user_id(authorization)
88+
tool_params = load_last_tool_config_impl(tool_id, tenant_id, user_id)
89+
return JSONResponse(
90+
status_code=HTTPStatus.OK,
91+
content={"message": tool_params, "status": "success"}
92+
)
93+
except ValueError:
94+
logger.error(f"Tool configuration not found for tool ID: {tool_id}")
95+
raise HTTPException(
96+
status_code=HTTPStatus.NOT_FOUND, detail="Tool configuration not found")
97+
except Exception as e:
98+
logger.error(f"Failed to load tool config: {e}")
99+
raise HTTPException(
100+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Failed to load tool config")

backend/apps/user_management_app.py

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from consts.exceptions import NoInviteCodeException, IncorrectInviteCodeException, UserRegistrationException
1212
from services.user_management_service import get_authorized_client, validate_token, \
1313
check_auth_service_health, signup_user, signin_user, refresh_user_token, \
14-
get_session_by_authorization
14+
get_session_by_authorization, revoke_regular_user
1515
from consts.exceptions import UnauthorizedError
1616
from utils.auth_utils import get_current_user_id
1717

@@ -69,7 +69,7 @@ async def signup(request: UserSignUpRequest):
6969
detail="EMAIL_ALREADY_EXISTS")
7070
except AuthWeakPasswordError as e:
7171
logging.error(f"User registration failed by weak password: {str(e)}")
72-
raise HTTPException(status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
72+
raise HTTPException(status_code=HTTPStatus.NOT_ACCEPTABLE,
7373
detail="WEAK_PASSWORD")
7474
except Exception as e:
7575
logging.error(f"User registration failed, unknown error: {str(e)}")
@@ -87,7 +87,7 @@ async def signin(request: UserSignInRequest):
8787
content=signin_content)
8888
except AuthApiError as e:
8989
logging.error(f"User login failed: {str(e)}")
90-
raise HTTPException(status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
90+
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED,
9191
detail="Email or password error")
9292
except Exception as e:
9393
logging.error(f"User login failed, unknown error: {str(e)}")
@@ -200,3 +200,48 @@ async def get_user_id(request: Request):
200200
logging.error(f"Get user ID failed: {str(e)}")
201201
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
202202
detail="Get user ID failed")
203+
204+
205+
@router.post("/revoke")
206+
async def revoke_user_account(request: Request):
207+
"""Delete current regular user's account and purge related data.
208+
209+
Notes:
210+
- Tenant admin (role=admin) is not allowed to be revoked via this endpoint.
211+
- Idempotent: local deletions are soft deletes; Supabase deletion may already have occurred.
212+
"""
213+
authorization = request.headers.get("Authorization")
214+
if not authorization:
215+
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED,
216+
detail="No authorization token provided")
217+
try:
218+
# Identify current user and tenant
219+
user_id, tenant_id = get_current_user_id(authorization)
220+
221+
# Determine role via token validation
222+
is_valid, user = validate_token(authorization.replace("Bearer ", ""))
223+
if not is_valid or not user:
224+
raise UnauthorizedError("User not logged in or session invalid")
225+
226+
# Extract role from user metadata
227+
user_role = "user"
228+
if getattr(user, "user_metadata", None) and 'role' in user.user_metadata:
229+
user_role = user.user_metadata['role']
230+
231+
# Disallow admin revocation by this endpoint
232+
if user_role == "admin":
233+
raise HTTPException(status_code=HTTPStatus.FORBIDDEN,
234+
detail="Admin account cannot be deleted via this endpoint")
235+
236+
# Orchestrate revoke for regular user
237+
await revoke_regular_user(user_id=user_id, tenant_id=tenant_id)
238+
239+
return JSONResponse(status_code=HTTPStatus.OK, content={"message": "User account revoked"})
240+
except UnauthorizedError as e:
241+
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail=str(e))
242+
except HTTPException:
243+
raise
244+
except Exception as e:
245+
logging.error(f"User revoke failed: {str(e)}")
246+
raise HTTPException(
247+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="User revoke failed")

backend/consts/const.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
# Supabase Configuration
3838
SUPABASE_URL = os.getenv('SUPABASE_URL')
3939
SUPABASE_KEY = os.getenv('SUPABASE_KEY')
40+
SERVICE_ROLE_KEY = os.getenv('SERVICE_ROLE_KEY', SUPABASE_KEY)
4041

4142

4243
# ===== To be migrated to frontend configuration =====
@@ -69,8 +70,6 @@
6970
DEFAULT_APP_DESCRIPTION_EN = "Nexent is an open-source agent SDK and platform, which can convert a single prompt into a complete multi-modal service - without orchestration, without complex drag-and-drop. Built on the MCP tool ecosystem, Nexent provides flexible model integration, scalable data processing, and powerful knowledge base management. Our goal is simple: to integrate data, models, and tools into a central intelligence hub, allowing anyone to easily integrate Nexent into their projects, making daily workflows smarter and more interconnected."
7071
DEFAULT_APP_NAME_ZH = "Nexent 智能体"
7172
DEFAULT_APP_NAME_EN = "Nexent Agent"
72-
DEFAULT_APP_ICON_URL = "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20shape-rendering%3D%22auto%22%20width%3D%2230%22%20height%3D%2230%22%3E%3Cmetadata%20xmlns%3Ardf%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2F02%2F22-rdf-syntax-ns%23%22%20xmlns%3Axsi%3D%22http%3A%2F%2Fwww.w3.org%2F2001%2FXMLSchema-instance%22%20xmlns%3Adc%3D%22http%3A%2F%2Fpurl.org%2Fdc%2Felements%2F1.1%2F%22%20xmlns%3Adcterms%3D%22http%3A%2F%2Fpurl.org%2Fdc%2Fterms%2F%22%3E%3Crdf%3ARDF%3E%3Crdf%3ADescription%3E%3Cdc%3Atitle%3EBootstrap%20Icons%3C%2Fdc%3Atitle%3E%3Cdc%3Acreator%3EThe%20Bootstrap%20Authors%3C%2Fdc%3Acreator%3E%3Cdc%3Asource%20xsi%3Atype%3D%22dcterms%3AURI%22%3Ehttps%3A%2F%2Fgithub.com%2Ftwbs%2Ficons%3C%2Fdc%3Asource%3E%3Cdcterms%3Alicense%20xsi%3Atype%3D%22dcterms%3AURI%22%3Ehttps%3A%2F%2Fgithub.com%2Ftwbs%2Ficons%2Fblob%2Fmain%2FLICENSE%3C%2Fdcterms%3Alicense%3E%3Cdc%3Arights%3E%E2%80%9EBootstrap%20Icons%E2%80%9D%20(https%3A%2F%2Fgithub.com%2Ftwbs%2Ficons)%20by%20%E2%80%9EThe%20Bootstrap%20Authors%E2%80%9D%2C%20licensed%20under%20%E2%80%9EMIT%E2%80%9D%20(https%3A%2F%2Fgithub.com%2Ftwbs%2Ficons%2Fblob%2Fmain%2FLICENSE)%3C%2Fdc%3Arights%3E%3C%2Frdf%3ADescription%3E%3C%2Frdf%3ARDF%3E%3C%2Fmetadata%3E%3Cmask%20id%3D%22viewboxMask%22%3E%3Crect%20width%3D%2224%22%20height%3D%2224%22%20rx%3D%2212%22%20ry%3D%2212%22%20x%3D%220%22%20y%3D%220%22%20fill%3D%22%23fff%22%20%2F%3E%3C%2Fmask%3E%3Cg%20mask%3D%22url(%23viewboxMask)%22%3E%3Crect%20fill%3D%22url(%23backgroundLinear)%22%20width%3D%2224%22%20height%3D%2224%22%20x%3D%220%22%20y%3D%220%22%20%2F%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22backgroundLinear%22%20gradientTransform%3D%22rotate(196%200.5%200.5)%22%3E%3Cstop%20stop-color%3D%22%232689cb%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%234226cb%22%2F%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Cg%20transform%3D%22translate(2.4000000000000004%202.4000000000000004)%20scale(0.8)%22%3E%3Cg%20transform%3D%22translate(4%204)%22%3E%3Cpath%20d%3D%22M11.742%2010.344a6.5%206.5%200%201%200-1.397%201.398h-.001c.03.04.062.078.098.115l3.85%203.85a1%201%200%200%200%201.415-1.414l-3.85-3.85a1.012%201.012%200%200%200-.115-.1v.001ZM12%206.5a5.5%205.5%200%201%201-11%200%205.5%205.5%200%200%201%2011%200Z%22%20fill%3D%22%23fff%22%2F%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E"
73-
7473

7574
# Minio Configuration
7675
MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT")
@@ -257,3 +256,6 @@
257256
os.getenv("LLM_SLOW_REQUEST_THRESHOLD_SECONDS", "5.0"))
258257
LLM_SLOW_TOKEN_RATE_THRESHOLD = float(
259258
os.getenv("LLM_SLOW_TOKEN_RATE_THRESHOLD", "10.0")) # tokens per second
259+
260+
# APP Version
261+
APP_VERSION = "v1.7.4"

backend/consts/model.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ class AgentInfoRequest(BaseModel):
204204
description: Optional[str] = None
205205
business_description: Optional[str] = None
206206
model_name: Optional[str] = None
207+
model_id: Optional[int] = None
207208
max_steps: Optional[int] = None
208209
provide_run_summary: Optional[bool] = None
209210
duty_prompt: Optional[str] = None
@@ -261,7 +262,6 @@ class ExportAndImportAgentInfo(BaseModel):
261262
display_name: Optional[str] = None
262263
description: str
263264
business_description: str
264-
model_name: str
265265
max_steps: int
266266
provide_run_summary: bool
267267
duty_prompt: Optional[str] = None

backend/database/conversation_db.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,67 @@ def delete_conversation(conversation_id: int, user_id: Optional[str] = None) ->
411411
return conversation_result.rowcount > 0
412412

413413

414+
def soft_delete_all_conversations_by_user(user_id: str) -> int:
415+
"""
416+
Soft-delete all conversations and related records created by a user.
417+
418+
Returns the number of conversations marked as deleted.
419+
"""
420+
with get_db_session() as session:
421+
update_data = {
422+
"delete_flag": 'Y',
423+
"update_time": func.current_timestamp()
424+
}
425+
426+
# 1) Find all conversation ids created by the user
427+
conv_ids = session.scalars(
428+
select(ConversationRecord.conversation_id).where(
429+
ConversationRecord.delete_flag == 'N',
430+
ConversationRecord.created_by == user_id,
431+
)
432+
).all()
433+
434+
if not conv_ids:
435+
return 0
436+
437+
# 2) Mark conversations as deleted
438+
session.execute(
439+
update(ConversationRecord)
440+
.where(ConversationRecord.conversation_id.in_(conv_ids), ConversationRecord.delete_flag == 'N')
441+
.values(update_data)
442+
)
443+
444+
# 3) Mark messages as deleted
445+
session.execute(
446+
update(ConversationMessage)
447+
.where(ConversationMessage.conversation_id.in_(conv_ids), ConversationMessage.delete_flag == 'N')
448+
.values(update_data)
449+
)
450+
451+
# 4) Mark message units as deleted
452+
session.execute(
453+
update(ConversationMessageUnit)
454+
.where(ConversationMessageUnit.conversation_id.in_(conv_ids), ConversationMessageUnit.delete_flag == 'N')
455+
.values(update_data)
456+
)
457+
458+
# 5) Mark search sources as deleted
459+
session.execute(
460+
update(ConversationSourceSearch)
461+
.where(ConversationSourceSearch.conversation_id.in_(conv_ids), ConversationSourceSearch.delete_flag == 'N')
462+
.values(update_data)
463+
)
464+
465+
# 6) Mark image sources as deleted
466+
session.execute(
467+
update(ConversationSourceImage)
468+
.where(ConversationSourceImage.conversation_id.in_(conv_ids), ConversationSourceImage.delete_flag == 'N')
469+
.values(update_data)
470+
)
471+
472+
return len(conv_ids)
473+
474+
414475
def update_message_opinion(message_id: int, opinion: str, user_id: Optional[str] = None) -> bool:
415476
"""
416477
Update message like/dislike status

backend/database/db_models.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,8 @@ class AgentInfo(TableBase):
191191
name = Column(String(100), doc="Agent name")
192192
display_name = Column(String(100), doc="Agent display name")
193193
description = Column(Text, doc="Description")
194-
model_name = Column(String(100), doc="Name of the model used")
194+
model_name = Column(String(100), doc="[DEPRECATED] Name of the model used, use model_id instead")
195+
model_id = Column(Integer, doc="Model ID, foreign key reference to model_record_t.model_id")
195196
max_steps = Column(Integer, doc="Maximum number of steps")
196197
duty_prompt = Column(Text, doc="Duty prompt content")
197198
constraint_prompt = Column(Text, doc="Constraint prompt content")

backend/database/memory_config_db.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,21 @@ def update_config_by_id(config_id: int, update_data: Dict[str, Any]) -> bool:
8888
except Exception:
8989
session.rollback()
9090
return False
91+
92+
93+
def soft_delete_all_configs_by_user_id(user_id: str, actor: str) -> bool:
94+
"""Soft-delete all memory user config records for a user."""
95+
with get_db_session() as session:
96+
try:
97+
session.query(MemoryUserConfig).filter(
98+
MemoryUserConfig.user_id == user_id,
99+
MemoryUserConfig.delete_flag == "N",
100+
).update({
101+
"delete_flag": "Y",
102+
"updated_by": actor,
103+
})
104+
session.commit()
105+
return True
106+
except Exception:
107+
session.rollback()
108+
return False

backend/database/tool_db.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,3 +213,14 @@ def delete_tools_by_agent_id(agent_id, tenant_id, user_id):
213213
).update({
214214
ToolInstance.delete_flag: 'Y', 'updated_by': user_id
215215
})
216+
217+
def search_last_tool_instance_by_tool_id(tool_id: int, tenant_id: str, user_id: str):
218+
with get_db_session() as session:
219+
query = session.query(ToolInstance).filter(
220+
ToolInstance.tool_id == tool_id,
221+
ToolInstance.tenant_id == tenant_id,
222+
ToolInstance.user_id == user_id,
223+
ToolInstance.delete_flag != 'Y'
224+
).order_by(ToolInstance.update_time.desc())
225+
tool_instance = query.first()
226+
return as_dict(tool_instance) if tool_instance else None

0 commit comments

Comments
 (0)