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
16 changes: 16 additions & 0 deletions backend/agents/create_agent_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,7 @@
allow_memory_search: bool = True,
version_no: int = 0,
override_model_id: int | None = None,
conversation_id: int = None,
request_requested_output_tokens: int | None = None,
tool_params: Optional[ToolParamsRequest | Dict[str, Any]] = None,
):
Expand Down Expand Up @@ -665,6 +666,7 @@
tenant_id,
user_id,
version_no=version_no,
conversation_id=conversation_id,
tool_params=normalized_tool_params,
)

Expand Down Expand Up @@ -947,6 +949,7 @@
tenant_id,
user_id,
version_no: int = 0,
conversation_id: int = None,
tool_params: Optional[ToolParamsRequest | Dict[str, Any]] = None,
):
tool_config_list = []
Expand Down Expand Up @@ -1085,6 +1088,17 @@
"storage_client": minio_client,
"validate_url_access": lambda urls: validate_urls_access(urls, user_id)
}
elif tool_config.class_name == "ScheduledTaskTool":
from database.scheduled_task_db import create_scheduled_task, query_tasks_by_agent, cancel_task
tool_config.metadata = {
"db_create": create_scheduled_task,
"db_list": query_tasks_by_agent,
"db_cancel": cancel_task,
"agent_id": agent_id,
"tenant_id": tenant_id,
"user_id": user_id,
"conversation_id": conversation_id,
}

tool_config_list.append(tool_config)

Expand Down Expand Up @@ -1338,19 +1352,20 @@


async def create_agent_run_info(
agent_id,
minio_files,
query,
history,
tenant_id: str,
user_id: str,
language: str = "zh",
allow_memory_search: bool = True,
is_debug: bool = False,
override_version_no: int | None = None,
override_model_id: int | None = None,
conversation_id: int = None,
requested_output_tokens: int | None = None,
tool_params: Optional[ToolParamsRequest | Dict[str, Any]] = None,

Check warning on line 1368 in backend/agents/create_agent_info.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Function "create_agent_run_info" has 14 parameters, which is greater than the 13 authorized.

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ798OxJLZGv1ddvq33V&open=AZ798OxJLZGv1ddvq33V&pullRequest=3216
):
# Determine which version_no to use based on is_debug flag
# If is_debug=false, use the current published version (current_version_no)
Expand Down Expand Up @@ -1379,6 +1394,7 @@
"last_user_query": final_query,
"allow_memory_search": allow_memory_search,
"version_no": version_no,
"conversation_id": conversation_id,
}
if override_model_id is not None:
create_config_kwargs["override_model_id"] = override_model_id
Expand Down
19 changes: 18 additions & 1 deletion backend/apps/app_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
FastAPI application factory with common configurations and exception handlers.
"""
import logging
from contextlib import asynccontextmanager
from typing import Callable, Awaitable, Optional

from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
Expand All @@ -21,6 +23,8 @@ def create_app(
cors_origins: list = None,
cors_methods: list = None,
enable_monitoring: bool = True,
lifespan_startup: Optional[Callable[[], Awaitable[None]]] = None,
lifespan_shutdown: Optional[Callable[[], Awaitable[None]]] = None,
) -> FastAPI:
"""
Create a FastAPI application with common configurations.
Expand All @@ -33,15 +37,28 @@ def create_app(
cors_origins: List of allowed CORS origins (default: ["*"])
cors_methods: List of allowed CORS methods (default: ["*"])
enable_monitoring: Whether to enable monitoring
lifespan_startup: Optional async startup hook (replaces @app.on_event("startup"))
lifespan_shutdown: Optional async shutdown hook (replaces @app.on_event("shutdown"))

Returns:
Configured FastAPI application
"""
@asynccontextmanager
async def _lifespan(_app: FastAPI):
if lifespan_startup is not None:
await lifespan_startup()
try:
yield
finally:
if lifespan_shutdown is not None:
await lifespan_shutdown()

app = FastAPI(
title=title,
description=description,
version=version,
root_path=root_path
root_path=root_path,
lifespan=_lifespan if (lifespan_startup or lifespan_shutdown) else None,
)

# Add CORS middleware
Expand Down
70 changes: 70 additions & 0 deletions backend/apps/conversation_management_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@
from typing import Any, Dict, Optional

from fastapi import APIRouter, Header, HTTPException, Request
from sqlalchemy.exc import SQLAlchemyError
from starlette.responses import JSONResponse

from consts.exceptions import UnauthorizedError
from consts.model import (
BatchMessageCheckRequest,
ConversationRequest,
ConversationResponse,
GenerateTitleRequest,
Expand All @@ -18,6 +22,7 @@
generate_conversation_title_service,
get_conversation_history_service,
get_conversation_list_service,
get_new_messages_service,
get_sources_service,
rename_conversation_service,
update_message_opinion_service, get_message_id_by_index_impl,
Expand Down Expand Up @@ -240,3 +245,68 @@
except Exception as e:
logging.error(f"Failed to get message ID: {str(e)}")
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))


@router.get("/{conversation_id}/new_messages", response_model=Dict[str, Any])
async def check_new_messages_endpoint(conversation_id: int, since_index: int = 0, authorization: Optional[str] = Header(None)):

Check warning on line 251 in backend/apps/conversation_management_app.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "Annotated" type hints for FastAPI dependency injection

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ6wr8UYOWwJzk61Ijpk&open=AZ6wr8UYOWwJzk61Ijpk&pullRequest=3216
"""
Lightweight polling: check if new messages exist for a single conversation.

Args:
conversation_id: Conversation ID
since_index: Last known message index on the client side
authorization: Authorization header

Returns:
Dict with has_new, max_index, since_index
"""
try:
user_id, tenant_id = get_current_user_id(authorization)

Check warning on line 264 in backend/apps/conversation_management_app.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace the unused local variable "tenant_id" with "_".

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ6wr8UYOWwJzk61Ijpj&open=AZ6wr8UYOWwJzk61Ijpj&pullRequest=3216
except (UnauthorizedError, ValueError) as e:
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail=str(e))
try:
result = get_new_messages_service(conversation_id, user_id, since_index)
return JSONResponse(status_code=HTTPStatus.OK, content=result)
except SQLAlchemyError as e:
logging.error(f"Failed to check new messages: {str(e)}")

Check failure on line 271 in backend/apps/conversation_management_app.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=AZ6wr8UYOWwJzk61Ijpl&open=AZ6wr8UYOWwJzk61Ijpl&pullRequest=3216
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))


@router.post("/batch_new_messages", response_model=Dict[str, Any])
async def batch_check_new_messages_endpoint(request: BatchMessageCheckRequest, authorization: Optional[str] = Header(None)):

Check warning on line 276 in backend/apps/conversation_management_app.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "Annotated" type hints for FastAPI dependency injection

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ74jbA9dUtZRCY7G7ni&open=AZ74jbA9dUtZRCY7G7ni&pullRequest=3216
"""
Batch check for new messages across multiple conversations.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[代码规范] batch_check_new_messages_endpoint 的请求体使用了 Dict[str, Any],缺少类型约束和输入验证。建议使用 Pydantic 模型定义请求结构(如 BatchMessageCheckRequest),确保 checks 列表中的每个元素都包含必要的字段和类型校验。


Args:
request: BatchMessageCheckRequest with a "checks" list of
{"conversation_id": int, "since_index": int}
authorization: Authorization header

Returns:
Dict mapping conversation_id to {has_new, max_index, since_index}
"""
try:
user_id, tenant_id = get_current_user_id(authorization)

Check warning on line 289 in backend/apps/conversation_management_app.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace the unused local variable "tenant_id" with "_".

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ6wr8UYOWwJzk61Ijpm&open=AZ6wr8UYOWwJzk61Ijpm&pullRequest=3216
except (UnauthorizedError, ValueError) as e:
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail=str(e))
try:
results = {}
for check in request.checks[:50]: # Cap at 50 to avoid abuse
# Isolate each conversation so one failure doesn't fail the
# whole batch; record an error marker instead.
try:
results[str(check.conversation_id)] = get_new_messages_service(
check.conversation_id, user_id, check.since_index
)
except SQLAlchemyError as e:
# Only swallow DB errors here so a single bad conversation
# doesn't fail the whole batch; other errors propagate.
logging.error(

Check failure on line 304 in backend/apps/conversation_management_app.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=AZ745cBZOTTT-9Dyj0Y4&open=AZ745cBZOTTT-9Dyj0Y4&pullRequest=3216
f"Failed to check new messages for conversation "
f"{check.conversation_id}: {str(e)}"
)
results[str(check.conversation_id)] = {"error": "check_failed"}
return JSONResponse(status_code=HTTPStatus.OK, content={"results": results})
except SQLAlchemyError as e:
logging.error(f"Failed to batch check new messages: {str(e)}")

Check failure on line 311 in backend/apps/conversation_management_app.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=AZ6wr8UYOWwJzk61Ijpo&open=AZ6wr8UYOWwJzk61Ijpo&pullRequest=3216
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
19 changes: 17 additions & 2 deletions backend/apps/runtime_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,27 @@
from apps.file_management_app import file_management_runtime_router as file_management_router
from apps.skill_app import skill_creator_router
from middleware.exception_handler import ExceptionHandlerMiddleware
from services.scheduled_task_scheduler import scheduled_task_scheduler

# Create logger instance
logger = logging.getLogger("runtime_app")

# Create FastAPI app with common configurations
app = create_app(title="Nexent Runtime API", description="Runtime APIs")

async def _start_scheduler():

Check warning on line 17 in backend/apps/runtime_app.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use asynchronous features in this function or remove the `async` keyword.

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ74jbBMdUtZRCY7G7nj&open=AZ74jbBMdUtZRCY7G7nj&pullRequest=3216
scheduled_task_scheduler.start()


async def _stop_scheduler():

Check warning on line 21 in backend/apps/runtime_app.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use asynchronous features in this function or remove the `async` keyword.

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ74jbBMdUtZRCY7G7nk&open=AZ74jbBMdUtZRCY7G7nk&pullRequest=3216
scheduled_task_scheduler.stop()


# Create FastAPI app with common configurations and scheduler lifecycle hooks
app = create_app(
title="Nexent Runtime API",
description="Runtime APIs",
lifespan_startup=_start_scheduler,
lifespan_shutdown=_stop_scheduler,
)

# Add global exception handler middleware
app.add_middleware(ExceptionHandlerMiddleware)
Expand Down
11 changes: 11 additions & 0 deletions backend/consts/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,17 @@ class MessageIdRequest(BaseModel):
message_index: int


class NewMessageCheckItem(BaseModel):
"""A single conversation to check for new messages in a batch request."""
conversation_id: int
since_index: int = 0


class BatchMessageCheckRequest(BaseModel):
"""Request body for batch new-message polling."""
checks: List[NewMessageCheckItem] = []


class ExportAndImportAgentInfo(BaseModel):
agent_id: int
tenant_id: Optional[str] = None
Expand Down
11 changes: 11 additions & 0 deletions backend/database/conversation_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -970,6 +970,17 @@ def get_source_searches_by_conversation(conversation_id: int, user_id: Optional[
return [as_dict(record) for record in search_records]


def get_max_message_index(conversation_id: int) -> int:
"""Return the maximum message_index for a conversation, or -1 if empty."""
with get_db_session() as session:
conversation_id = int(conversation_id)
stmt = select(func.coalesce(func.max(ConversationMessage.message_index), -1)).where(
ConversationMessage.conversation_id == conversation_id,
ConversationMessage.delete_flag == 'N',
)
return session.execute(stmt).scalar()


def get_message(message_id: int, user_id: Optional[str] = None) -> Dict[str, Any]:
"""
Get message details by message ID
Expand Down
29 changes: 29 additions & 0 deletions backend/database/db_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1289,6 +1289,35 @@ class A2AMessage(SimpleTableBase):
timezone=False), server_default=func.now(), doc="Message creation timestamp")


class ScheduledTaskRecord(TableBase):
"""
Scheduled task records for deferred / recurring agent execution.
"""
__tablename__ = "scheduled_tasks_t"
__table_args__ = (
Index("ix_scheduled_task_status_next_fire", "status", "next_fire_time"),
Index("ix_scheduled_task_agent_delete", "agent_id", "delete_flag"),
{"schema": SCHEMA},
)

task_id = Column(Integer, Sequence("scheduled_tasks_t_task_id_seq", schema=SCHEMA),
primary_key=True, nullable=False, autoincrement=True, doc="Primary key")
task_uuid = Column(String(36), unique=True, nullable=False, doc="Unique task identifier (UUID)")
task_name = Column(String(200), doc="Human-readable task name")
task_prompt = Column(Text, nullable=False, doc="The prompt to execute when the task fires")
task_type = Column(String(10), nullable=False, doc="Task type: oneshot or cron")
cron_expression = Column(String(100), doc="Cron expression for recurring tasks")
delay_seconds = Column(Integer, doc="Delay in seconds for oneshot tasks")
status = Column(String(20), default="pending", doc="Task status: pending, fired, cancelled, error")
next_fire_time = Column(TIMESTAMP(timezone=False), doc="Next scheduled execution time")
fire_count = Column(Integer, default=0, doc="Number of times this task has fired")
max_fires = Column(Integer, nullable=True, doc="Maximum number of fires (NULL = unlimited)")
agent_id = Column(Integer, nullable=False, doc="Agent ID that owns this task")
conversation_id = Column(Integer, nullable=True, doc="Conversation ID associated with this task")
tenant_id = Column(String(100), nullable=False, doc="Tenant ID for multi-tenancy isolation")
user_id = Column(String(100), nullable=False, doc="User ID who created this task")


class A2AArtifact(SimpleTableBase):
"""
A2A artifacts. Stores the output/artifacts produced by a task.
Expand Down
Loading