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
70 changes: 69 additions & 1 deletion backend/apps/agent_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from http import HTTPStatus
from typing import Optional

from fastapi import APIRouter, Body, Header, HTTPException, Request, Query
from fastapi import APIRouter, Body, File, Header, HTTPException, Request, Query, UploadFile
from fastapi.encoders import jsonable_encoder
from starlette.responses import JSONResponse, Response, StreamingResponse

Expand All @@ -14,6 +14,8 @@
AgentIDRequest,
ConversationResponse,
AgentImportRequest,
AgentBatchExportRequest,
AgentBatchImportResult,
AgentNameBatchCheckRequest,
AgentNameBatchRegenerateRequest,
VersionPublishRequest,
Expand Down Expand Up @@ -51,6 +53,8 @@
get_agent_by_name_impl,
export_agent_with_skills_impl,
import_agent_with_skills_impl,
export_agents_batch_impl,
import_agents_batch_impl,
)
from services.prompt_service import generate_guardrail_rules_impl
from services.nl2agent_service import create_nl2agent_stream
Expand Down Expand Up @@ -354,6 +358,70 @@ async def import_agent_api(request: AgentImportRequest, authorization: Optional[
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Agent import error.")


@agent_config_router.post("/export/batch")
async def export_agents_batch_api(
request: AgentBatchExportRequest,
authorization: Optional[str] = Header(None)
):
"""
Batch export multiple agents into a single ZIP archive.

The archive contains ``manifest.json`` plus one folder per agent, each
holding the standard ``agent.json`` (and optional ``skills/*.zip``).
"""
try:
if not request.agent_ids:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="At least one agent_id is required for batch export.")
result = await export_agents_batch_impl(
request.agent_ids, authorization)
return Response(
content=result["data"],
media_type="application/zip",
headers={
"Content-Disposition": f"attachment; filename=\"{result.get('filename', 'agents_batch_export.zip')}\""
}
)
except HTTPException:
raise
except Exception as e:
logger.exception(f"Agent batch export error: {str(e)}")
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Agent batch export error.")


@agent_config_router.post("/import/batch", response_model=AgentBatchImportResult)
async def import_agents_batch_api(
file: UploadFile = File(...),
authorization: Optional[str] = Header(None)
):
"""
Batch import agents from a ZIP archive produced by /agent/export/batch.

Each agent in the archive is imported independently; per-agent failures are
captured in the returned summary instead of aborting the whole request.
"""
try:
zip_bytes = await file.read()
if not zip_bytes:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="Empty ZIP file received.")
summary = await import_agents_batch_impl(zip_bytes, authorization)
return JSONResponse(status_code=HTTPStatus.OK, content=summary)
except ValueError as e:
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
except HTTPException:
raise
except Exception as e:
logger.exception(f"Agent batch import error: {str(e)}")
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Agent batch 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)):
"""
Expand Down
5 changes: 5 additions & 0 deletions backend/consts/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,11 @@ class SkillDuplicateError(Exception):
"""Raised when importing an agent with skills that have duplicate names in target tenant."""
def __init__(self, duplicate_names: List[str]):
self.duplicate_names = duplicate_names
names_str = ", ".join(duplicate_names)
super().__init__(
f"Skill name conflict: the following skills already exist in your workspace: {names_str}. "
f"Please rename them or delete the existing skills before importing."
)


class SkillException(Exception):
Expand Down
21 changes: 21 additions & 0 deletions backend/consts/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -870,6 +870,27 @@ class AgentImportRequest(BaseModel):
skills: Optional[List[SkillZipEntry]] = None


class AgentBatchExportRequest(BaseModel):
"""Request body for batch agent export. Returns a ZIP archive."""
agent_ids: List[int]


class AgentBatchImportResultItem(BaseModel):
"""Single agent result entry inside a batch import summary."""
name: str
display_name: Optional[str] = None
success: bool
error: Optional[str] = None


class AgentBatchImportResult(BaseModel):
"""Summary returned after importing a batch agent ZIP archive."""
total: int
success_count: int
failed_count: int
items: List[AgentBatchImportResultItem]


class AgentNameBatchRegenerateItem(BaseModel):
name: str
display_name: Optional[str] = None
Expand Down
Loading
Loading