mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-04 18:13:03 +00:00
The backend import package is now docsgpt, the name it will carry on PyPI; application was far too generic to install into anyone's site-packages. git mv plus a mechanical rewrite of every import, dotted string and path reference: 734 Python files, the compose files, Dockerfile, workflows, docs, setup scripts, devcontainer, k8s manifests, vscode config, pytest and coverage config, .gitignore. Behaviour is unchanged. Kept for one release: - A top-level application package whose meta-path finder resolves application.x.y to the already-imported docsgpt.x.y object, so old imports and entry points (celery -A application.app.celery, uvicorn application.asgi:asgi_app) keep working with a FutureWarning. - Celery registers every application.* task name as an alias of its docsgpt.* task on start-up, so messages queued by the previous release still run. The redbeat key prefix moves to redbeat:docsgpt:v2: so schedule entries the previous release wrote are left unread instead of firing twice. The backend image builds from the repository root (docker build -f docsgpt/Dockerfile .) so it can ship the alias package; a root .dockerignore allow-lists docsgpt/ and application/ and keeps caches, local data, .env files, the sample index files and the Dockerfile out. Compose and the image workflows point at the new context.
367 lines
15 KiB
Python
367 lines
15 KiB
Python
"""
|
|
Agent folders management routes.
|
|
Provides virtual folder organization for agents (Google Drive-like structure).
|
|
"""
|
|
|
|
from flask import current_app, jsonify, make_response, request
|
|
from flask_restx import Namespace, Resource, fields
|
|
from sqlalchemy import text as _sql_text
|
|
|
|
from docsgpt.api import api
|
|
from docsgpt.storage.db.base_repository import looks_like_uuid
|
|
from docsgpt.storage.db.repositories.agent_folders import AgentFoldersRepository
|
|
from docsgpt.storage.db.repositories.agents import AgentsRepository
|
|
from docsgpt.storage.db.session import db_readonly, db_session
|
|
|
|
|
|
agents_folders_ns = Namespace(
|
|
"agents_folders", description="Agent folder management", path="/api/agents/folders"
|
|
)
|
|
|
|
|
|
def _resolve_folder_id(repo: AgentFoldersRepository, folder_id: str, user: str):
|
|
"""Resolve a folder id that may be either a UUID or legacy Mongo ObjectId."""
|
|
if not folder_id:
|
|
return None
|
|
if looks_like_uuid(folder_id):
|
|
row = repo.get(folder_id, user)
|
|
if row is not None:
|
|
return row
|
|
return repo.get_by_legacy_id(folder_id, user)
|
|
|
|
|
|
def _folder_error_response(message: str, err: Exception):
|
|
current_app.logger.error(f"{message}: {err}", exc_info=True)
|
|
return make_response(jsonify({"success": False, "message": message}), 400)
|
|
|
|
|
|
def _serialize_folder(f: dict) -> dict:
|
|
created_at = f.get("created_at")
|
|
updated_at = f.get("updated_at")
|
|
return {
|
|
"id": str(f["id"]),
|
|
"name": f.get("name"),
|
|
"parent_id": str(f["parent_id"]) if f.get("parent_id") else None,
|
|
"created_at": created_at.isoformat() if hasattr(created_at, "isoformat") else created_at,
|
|
"updated_at": updated_at.isoformat() if hasattr(updated_at, "isoformat") else updated_at,
|
|
}
|
|
|
|
|
|
@agents_folders_ns.route("/")
|
|
class AgentFolders(Resource):
|
|
@api.doc(description="Get all folders for the user")
|
|
def get(self):
|
|
decoded_token = request.decoded_token
|
|
if not decoded_token:
|
|
return make_response(jsonify({"success": False}), 401)
|
|
user = decoded_token.get("sub")
|
|
try:
|
|
with db_readonly() as conn:
|
|
folders = AgentFoldersRepository(conn).list_for_user(user)
|
|
result = [_serialize_folder(f) for f in folders]
|
|
return make_response(jsonify({"folders": result}), 200)
|
|
except Exception as err:
|
|
return _folder_error_response("Failed to fetch folders", err)
|
|
|
|
@api.doc(description="Create a new folder")
|
|
@api.expect(
|
|
api.model(
|
|
"CreateFolder",
|
|
{
|
|
"name": fields.String(required=True, description="Folder name"),
|
|
"parent_id": fields.String(required=False, description="Parent folder ID"),
|
|
},
|
|
)
|
|
)
|
|
def post(self):
|
|
decoded_token = request.decoded_token
|
|
if not decoded_token:
|
|
return make_response(jsonify({"success": False}), 401)
|
|
user = decoded_token.get("sub")
|
|
data = request.get_json()
|
|
if not data or not data.get("name"):
|
|
return make_response(jsonify({"success": False, "message": "Folder name is required"}), 400)
|
|
|
|
parent_id_input = data.get("parent_id")
|
|
description = data.get("description")
|
|
|
|
try:
|
|
with db_session() as conn:
|
|
repo = AgentFoldersRepository(conn)
|
|
pg_parent_id = None
|
|
if parent_id_input:
|
|
parent = _resolve_folder_id(repo, parent_id_input, user)
|
|
if not parent:
|
|
return make_response(
|
|
jsonify({"success": False, "message": "Parent folder not found"}),
|
|
404,
|
|
)
|
|
pg_parent_id = str(parent["id"])
|
|
folder = repo.create(
|
|
user, data["name"],
|
|
description=description,
|
|
parent_id=pg_parent_id,
|
|
)
|
|
return make_response(
|
|
jsonify(
|
|
{
|
|
"id": str(folder["id"]),
|
|
"name": folder["name"],
|
|
"parent_id": pg_parent_id,
|
|
}
|
|
),
|
|
201,
|
|
)
|
|
except Exception as err:
|
|
return _folder_error_response("Failed to create folder", err)
|
|
|
|
|
|
@agents_folders_ns.route("/<string:folder_id>")
|
|
class AgentFolder(Resource):
|
|
@api.doc(description="Get a specific folder with its agents")
|
|
def get(self, folder_id):
|
|
decoded_token = request.decoded_token
|
|
if not decoded_token:
|
|
return make_response(jsonify({"success": False}), 401)
|
|
user = decoded_token.get("sub")
|
|
try:
|
|
with db_readonly() as conn:
|
|
folders_repo = AgentFoldersRepository(conn)
|
|
folder = _resolve_folder_id(folders_repo, folder_id, user)
|
|
if not folder:
|
|
return make_response(
|
|
jsonify({"success": False, "message": "Folder not found"}),
|
|
404,
|
|
)
|
|
pg_folder_id = str(folder["id"])
|
|
|
|
agents_rows = conn.execute(
|
|
_sql_text(
|
|
"SELECT id, name, description FROM agents "
|
|
"WHERE user_id = :user_id AND folder_id = CAST(:fid AS uuid) "
|
|
"ORDER BY created_at DESC"
|
|
),
|
|
{"user_id": user, "fid": pg_folder_id},
|
|
).fetchall()
|
|
agents_list = [
|
|
{
|
|
"id": str(row._mapping["id"]),
|
|
"name": row._mapping["name"],
|
|
"description": row._mapping.get("description", "") or "",
|
|
}
|
|
for row in agents_rows
|
|
]
|
|
|
|
subfolders = folders_repo.list_children(pg_folder_id, user)
|
|
subfolders_list = [
|
|
{"id": str(sf["id"]), "name": sf["name"]}
|
|
for sf in subfolders
|
|
]
|
|
|
|
return make_response(
|
|
jsonify(
|
|
{
|
|
"id": pg_folder_id,
|
|
"name": folder["name"],
|
|
"parent_id": (
|
|
str(folder["parent_id"]) if folder.get("parent_id") else None
|
|
),
|
|
"agents": agents_list,
|
|
"subfolders": subfolders_list,
|
|
}
|
|
),
|
|
200,
|
|
)
|
|
except Exception as err:
|
|
return _folder_error_response("Failed to fetch folder", err)
|
|
|
|
@api.doc(description="Update a folder")
|
|
def put(self, folder_id):
|
|
decoded_token = request.decoded_token
|
|
if not decoded_token:
|
|
return make_response(jsonify({"success": False}), 401)
|
|
user = decoded_token.get("sub")
|
|
data = request.get_json()
|
|
if not data:
|
|
return make_response(jsonify({"success": False, "message": "No data provided"}), 400)
|
|
|
|
try:
|
|
with db_session() as conn:
|
|
repo = AgentFoldersRepository(conn)
|
|
folder = _resolve_folder_id(repo, folder_id, user)
|
|
if not folder:
|
|
return make_response(
|
|
jsonify({"success": False, "message": "Folder not found"}),
|
|
404,
|
|
)
|
|
pg_folder_id = str(folder["id"])
|
|
|
|
update_fields: dict = {}
|
|
if "name" in data:
|
|
update_fields["name"] = data["name"]
|
|
if "description" in data:
|
|
update_fields["description"] = data["description"]
|
|
if "parent_id" in data:
|
|
parent_input = data.get("parent_id")
|
|
if parent_input:
|
|
if parent_input == folder_id or parent_input == pg_folder_id:
|
|
return make_response(
|
|
jsonify(
|
|
{
|
|
"success": False,
|
|
"message": "Cannot set folder as its own parent",
|
|
}
|
|
),
|
|
400,
|
|
)
|
|
parent = _resolve_folder_id(repo, parent_input, user)
|
|
if not parent:
|
|
return make_response(
|
|
jsonify({"success": False, "message": "Parent folder not found"}),
|
|
404,
|
|
)
|
|
if str(parent["id"]) == pg_folder_id:
|
|
return make_response(
|
|
jsonify(
|
|
{
|
|
"success": False,
|
|
"message": "Cannot set folder as its own parent",
|
|
}
|
|
),
|
|
400,
|
|
)
|
|
update_fields["parent_id"] = str(parent["id"])
|
|
else:
|
|
update_fields["parent_id"] = None
|
|
|
|
if update_fields:
|
|
repo.update(pg_folder_id, user, update_fields)
|
|
|
|
return make_response(jsonify({"success": True}), 200)
|
|
except Exception as err:
|
|
return _folder_error_response("Failed to update folder", err)
|
|
|
|
@api.doc(description="Delete a folder")
|
|
def delete(self, folder_id):
|
|
decoded_token = request.decoded_token
|
|
if not decoded_token:
|
|
return make_response(jsonify({"success": False}), 401)
|
|
user = decoded_token.get("sub")
|
|
try:
|
|
with db_session() as conn:
|
|
repo = AgentFoldersRepository(conn)
|
|
folder = _resolve_folder_id(repo, folder_id, user)
|
|
if not folder:
|
|
return make_response(
|
|
jsonify({"success": False, "message": "Folder not found"}),
|
|
404,
|
|
)
|
|
pg_folder_id = str(folder["id"])
|
|
# Clear folder assignments from agents; self-FK
|
|
# ``ON DELETE SET NULL`` handles child folders.
|
|
AgentsRepository(conn).clear_folder_for_all(pg_folder_id, user)
|
|
deleted = repo.delete(pg_folder_id, user)
|
|
if not deleted:
|
|
return make_response(
|
|
jsonify({"success": False, "message": "Folder not found"}),
|
|
404,
|
|
)
|
|
return make_response(jsonify({"success": True}), 200)
|
|
except Exception as err:
|
|
return _folder_error_response("Failed to delete folder", err)
|
|
|
|
|
|
@agents_folders_ns.route("/move_agent")
|
|
class MoveAgentToFolder(Resource):
|
|
@api.doc(description="Move an agent to a folder or remove from folder")
|
|
@api.expect(
|
|
api.model(
|
|
"MoveAgent",
|
|
{
|
|
"agent_id": fields.String(required=True, description="Agent ID to move"),
|
|
"folder_id": fields.String(required=False, description="Target folder ID (null to remove from folder)"),
|
|
},
|
|
)
|
|
)
|
|
def post(self):
|
|
decoded_token = request.decoded_token
|
|
if not decoded_token:
|
|
return make_response(jsonify({"success": False}), 401)
|
|
user = decoded_token.get("sub")
|
|
data = request.get_json()
|
|
if not data or not data.get("agent_id"):
|
|
return make_response(jsonify({"success": False, "message": "Agent ID is required"}), 400)
|
|
|
|
agent_id_input = data["agent_id"]
|
|
folder_id_input = data.get("folder_id")
|
|
|
|
try:
|
|
with db_session() as conn:
|
|
agents_repo = AgentsRepository(conn)
|
|
agent = agents_repo.get_any(agent_id_input, user)
|
|
if not agent:
|
|
return make_response(
|
|
jsonify({"success": False, "message": "Agent not found"}),
|
|
404,
|
|
)
|
|
pg_folder_id = None
|
|
if folder_id_input:
|
|
folders_repo = AgentFoldersRepository(conn)
|
|
folder = _resolve_folder_id(folders_repo, folder_id_input, user)
|
|
if not folder:
|
|
return make_response(
|
|
jsonify({"success": False, "message": "Folder not found"}),
|
|
404,
|
|
)
|
|
pg_folder_id = str(folder["id"])
|
|
agents_repo.set_folder(str(agent["id"]), user, pg_folder_id)
|
|
return make_response(jsonify({"success": True}), 200)
|
|
except Exception as err:
|
|
return _folder_error_response("Failed to move agent", err)
|
|
|
|
|
|
@agents_folders_ns.route("/bulk_move")
|
|
class BulkMoveAgents(Resource):
|
|
@api.doc(description="Move multiple agents to a folder")
|
|
@api.expect(
|
|
api.model(
|
|
"BulkMoveAgents",
|
|
{
|
|
"agent_ids": fields.List(fields.String, required=True, description="List of agent IDs"),
|
|
"folder_id": fields.String(required=False, description="Target folder ID"),
|
|
},
|
|
)
|
|
)
|
|
def post(self):
|
|
decoded_token = request.decoded_token
|
|
if not decoded_token:
|
|
return make_response(jsonify({"success": False}), 401)
|
|
user = decoded_token.get("sub")
|
|
data = request.get_json()
|
|
if not data or not data.get("agent_ids"):
|
|
return make_response(jsonify({"success": False, "message": "Agent IDs are required"}), 400)
|
|
|
|
agent_ids = data["agent_ids"]
|
|
folder_id_input = data.get("folder_id")
|
|
|
|
try:
|
|
with db_session() as conn:
|
|
agents_repo = AgentsRepository(conn)
|
|
pg_folder_id = None
|
|
if folder_id_input:
|
|
folders_repo = AgentFoldersRepository(conn)
|
|
folder = _resolve_folder_id(folders_repo, folder_id_input, user)
|
|
if not folder:
|
|
return make_response(
|
|
jsonify({"success": False, "message": "Folder not found"}),
|
|
404,
|
|
)
|
|
pg_folder_id = str(folder["id"])
|
|
for agent_id_input in agent_ids:
|
|
agent = agents_repo.get_any(agent_id_input, user)
|
|
if agent is not None:
|
|
agents_repo.set_folder(str(agent["id"]), user, pg_folder_id)
|
|
return make_response(jsonify({"success": True}), 200)
|
|
except Exception as err:
|
|
return _folder_error_response("Failed to move agents", err)
|