tek Atrust
chore(deploy): build monolithic server for Hugging Face
d5ef46f
Raw
History Blame Contribute Delete
8.23 kB
"""
Projects Ops API - Handles Tasks, AI Dispatching, and Agent Callbacks.
"""
from typing import Any, cast
from fastapi import APIRouter, Depends, HTTPException, Request, Response
from src.server.schemas.projects import (
AgentOutputUpdateRequest,
AgentStatusUpdateRequest,
CreateTaskRequest,
GenerateTaskFromAlertRequest,
RefineTaskRequest,
UpdateTaskRequest,
)
from ...auth.dependencies import get_current_user, requires_permission
from ...auth.permissions import AGENT_TRIGGER_DEV
from ...services.profile_service import ProfileService
from ...services.projects.task_service import TaskService
from ...services.rbac_service import RBACService
from ...utils.api_utils import handle_service_result
from ...utils.etag_utils import check_etag, generate_etag
router = APIRouter()
def _err(res: Any, code: int = 500):
detail = res.get("error", res) if isinstance(res, dict) else res
raise HTTPException(status_code=code, detail=detail)
@router.get("/projects/task-counts")
async def get_all_task_counts(request: Request, response: Response, current_user: dict = Depends(get_current_user)):
s, res = await TaskService().get_all_project_task_counts()
if not s:
_err(res)
etag = generate_etag({"counts": res, "count": len(res if isinstance(res, dict) else [])})
response.headers["ETag"] = etag
if check_etag(request.headers.get("If-None-Match"), etag):
response.status_code = 304
return None
return res
@router.get("/projects/{project_id}/tasks")
async def list_project_tasks(
project_id: str,
req: Request,
response: Response,
include_archived: bool = False,
exclude_large_fields: bool = False,
current_user: dict = Depends(get_current_user),
):
s, res = await TaskService().list_tasks(
project_id=project_id,
include_closed=True,
exclude_large_fields=exclude_large_fields,
include_archived=include_archived,
)
if not s or not isinstance(res, dict):
_err(res)
tasks = res.get("tasks", [])
etag = generate_etag({"tasks": tasks, "project_id": project_id})
response.headers["ETag"] = etag
if check_etag(req.headers.get("If-None-Match"), etag):
response.status_code = 304
return None
return tasks
@router.post("/tasks/refine-description")
async def refine_task_description(req: RefineTaskRequest, current_user: dict = Depends(get_current_user)):
res = await TaskService().refine_task_description(req.title, req.description)
return {"refined_description": res}
@router.post("/tasks/generate-from-alert")
async def generate_task_from_alert(
req: GenerateTaskFromAlertRequest, current_user: dict = Depends(requires_permission(AGENT_TRIGGER_DEV))
):
s, res = await TaskService().generate_task_from_alert(alert_id=req.alert_id, assignee_id=req.assignee_id)
return handle_service_result(s, res)
@router.post("/tasks")
async def create_task(req: CreateTaskRequest, current_user: dict = Depends(get_current_user)):
"""Creates a new task. Includes cross-department assignment blocking."""
u_role = current_user.get("role", "viewer").lower()
u_dept = current_user.get("department")
target_name, res_id = req.assignee, req.assignee_id
if res_id:
from src.server.services.shared_constants import AI_AGENT_ROLES
if res_id in AI_AGENT_ROLES.values():
res_id = None
for n, aid in AI_AGENT_ROLES.items():
if aid == req.assignee_id:
target_name = n.split(" ")[0]
break
else:
ok, p = ProfileService().get_profile(str(req.assignee_id))
if ok and isinstance(p, dict):
target_name = str(p.get("name"))
if u_role not in ["system_admin", "admin"] and p.get("department") != u_dept:
_err(f"Cannot assign tasks to members outside your department ({p.get('department')})", 403)
s, res = await TaskService().create_task(
project_id=req.project_id,
title=req.title,
description=req.description or "",
assignee=target_name or "User",
assignee_id=res_id,
priority=req.priority or "medium",
task_order=req.task_order or 0,
feature=req.feature,
due_date=req.due_date,
knowledge_source_ids=req.knowledge_source_ids,
is_recurring=req.is_recurring or False,
crawler_target_id=req.crawler_target_id,
schedule_config=req.schedule_config,
)
if not s or not isinstance(res, dict):
_err(res, 400)
return {"message": "Task created successfully", "task": res.get("task")}
@router.get("/tasks")
async def list_tasks(
status: str | None = None,
project_id: str | None = None,
assignee_id: str | None = None,
include_closed: bool = False,
include_unassigned: bool = False,
page: int = 1,
per_page: int = 50,
exclude_large_fields: bool = False,
current_user: dict = Depends(get_current_user),
):
u_role = current_user.get("role", "member").lower()
u_id = current_user.get("id")
if u_role in ["system_admin", "admin", "manager"]:
a_filter = assignee_id
else:
a_filter = u_id
s, res = await TaskService().list_tasks(
project_id=project_id if project_id and project_id.lower() != "all" else None,
status=status or "",
include_closed=include_closed,
exclude_large_fields=exclude_large_fields,
assignee_id=a_filter,
include_unassigned=include_unassigned if u_role in ["admin", "manager"] else False,
)
data = cast(dict[str, Any], handle_service_result(s, res))
tasks = data.get("tasks", [])
return {
"tasks": tasks[(page - 1) * per_page : page * per_page],
"pagination": {
"total": len(tasks),
"page": page,
"per_page": per_page,
"pages": (len(tasks) + per_page - 1) // per_page,
},
}
@router.get("/tasks/{task_id}")
async def get_task(task_id: str, current_user: dict = Depends(get_current_user)):
s, res = await TaskService().get_task(task_id)
return cast(dict[str, Any], handle_service_result(s, res)).get("task")
@router.put("/tasks/{task_id}")
async def update_task(task_id: str, req: UpdateTaskRequest, current_user: dict = Depends(get_current_user)):
u_role = current_user.get("role", "viewer").lower()
fields = {k: v for k, v in req.model_dump().items() if v is not None}
if "assignee" in fields or "assignee_id" in fields:
name = fields.get("assignee")
if name and name != "Unassigned":
ok, r = ProfileService().get_user_role(str(name))
if ok and r and not await RBACService().has_permission_to_assign(u_role, r):
_err("Forbidden: Cannot reassign to this role", 403)
s, res = await TaskService().update_task(task_id, fields)
return {
"message": "Task updated successfully",
"task": cast(dict[str, Any], handle_service_result(s, res)).get("task"),
}
@router.delete("/tasks/{task_id}")
async def delete_task(task_id: str, current_user: dict = Depends(get_current_user)):
if current_user.get("role", "member").lower() not in ["system_admin", "admin", "manager"]:
_err("Forbidden: Only managers can archive tasks", 403)
s, res = await TaskService().archive_task(task_id, archived_by=str(current_user.get("id")))
if not s:
_err(res, 400)
return {"message": "Task archived successfully"}
@router.post("/tasks/{task_id}/agent-status", tags=["Agent Callback"])
async def report_task_status_from_agent(task_id: str, req: AgentStatusUpdateRequest):
s, res = await TaskService().update_task_status_from_agent(
task_id=task_id, new_status=req.status, agent_id=req.agent_id
)
if not s:
_err(res, 400)
return res
@router.post("/tasks/{task_id}/agent-output", tags=["Agent Callback"])
async def report_task_output_from_agent(task_id: str, req: AgentOutputUpdateRequest):
s, res = await TaskService().save_agent_output(task_id=task_id, output=req.output, agent_id=req.agent_id)
if not s:
_err(res, 400)
return res