Add task tool result bridge and evidence attachment workflow

This commit is contained in:
A R R R Associates
2026-08-31 15:24:09 +05:30
parent fbb8607842
commit a362cf438c
6 changed files with 497 additions and 6 deletions
+157 -3
View File
@@ -1,7 +1,7 @@
from __future__ import annotations
from fastapi import APIRouter, Form, Request
from fastapi.responses import RedirectResponse
from fastapi import APIRouter, File, Form, Request, UploadFile
from fastapi.responses import RedirectResponse, Response
from sqlalchemy import select
from app.core.db.common import CommonSessionLocal
@@ -11,7 +11,13 @@ from app.core.templating import templates
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
from app.modules.core.iam.models import User
from app.modules.core.rbac.permission_guard import require_permission
from app.modules.services.task_tools import get_task_tool
from app.modules.services.task_tools import build_task_tool_launch_url, get_task_tool
from app.modules.services.task_tool_results import (
get_task_tool_run,
list_task_tool_runs,
record_task_tool_result,
start_task_tool_run,
)
from app.modules.services.execution import (
TASK_COMMENT_TYPES,
TASK_COMMENT_VISIBILITIES,
@@ -445,6 +451,154 @@ def task_edit_page(request: Request, task_id: int):
can_manage_fields=_can_bulk_manage_tasks(db, user),
can_reassign=_can_assign_staff(db, user),
task_tool=get_task_tool(getattr(task, "task_tool_code", "NONE")),
tool_runs=list_task_tool_runs(db, tenant_id=tenant_id, task_id=task.id),
)
finally:
db.close()
@router.post("/tasks/{task_id}/tool/start")
def task_tool_start(request: Request, task_id: int, csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
tenant_id = _active_tenant_id(request, user)
branch_id = _active_branch_id(request, user, db)
task = get_task(
db,
tenant_id=tenant_id,
task_id=task_id,
branch_id=branch_id,
assigned_to_user_id=_assigned_user_filter(db, user),
partner_user_id=_partner_visibility_user_id(db, user),
financial_year=_active_financial_year(request),
)
if not task:
return RedirectResponse(url="/services/work-tracker", status_code=303)
tool = get_task_tool(getattr(task, "task_tool_code", "NONE"))
if tool.code == "NONE" or not tool.launch_url:
return RedirectResponse(url=f"/services/work-tracker/tasks/{task.id}/edit?tool_error=not_configured", status_code=303)
run = start_task_tool_run(db, task=task, user_id=int(user.id))
launch_url = build_task_tool_launch_url(
tool,
client_id=int(task.client_id),
task_id=int(task.id),
tool_run_id=int(run.id),
)
return RedirectResponse(url=launch_url, status_code=303)
finally:
db.close()
@router.post("/tool-runs/{run_id}/result")
def task_tool_result_submit(
request: Request,
run_id: int,
status: str = Form("completed"),
summary: str = Form(""),
result_json: str = Form(""),
clean_pass: str = Form(""),
exception_count: str = Form(""),
output_file: UploadFile | None = File(None),
csrf_token: str = Form(...),
):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
tenant_id = _active_tenant_id(request, user)
run = get_task_tool_run(db, tenant_id=tenant_id, run_id=run_id)
if not run:
return RedirectResponse(url="/services/work-tracker", status_code=303)
branch_id = _active_branch_id(request, user, db)
task = get_task(
db,
tenant_id=tenant_id,
task_id=int(run.task_instance_id),
branch_id=branch_id,
assigned_to_user_id=_assigned_user_filter(db, user),
partner_user_id=_partner_visibility_user_id(db, user),
financial_year=_active_financial_year(request),
)
if not task:
return _redirect_denied()
clean_value = None
if clean_pass.strip().lower() in {"1", "true", "yes", "y", "on"}:
clean_value = True
elif clean_pass.strip().lower() in {"0", "false", "no", "n", "off"}:
clean_value = False
exception_value = None
if exception_count.strip():
try:
exception_value = max(0, int(exception_count))
except ValueError:
return RedirectResponse(url=f"/services/work-tracker/tasks/{task.id}/edit?tool_error=invalid_exception_count", status_code=303)
file_bytes = None
filename = None
content_type = None
if output_file is not None and getattr(output_file, "filename", None):
file_bytes = output_file.file.read()
filename = str(output_file.filename or "tool-output.bin")
content_type = str(output_file.content_type or "application/octet-stream")
try:
record_task_tool_result(
db,
run=run,
status=status,
summary=summary,
result_payload=result_json,
clean_pass=clean_value,
exception_count=exception_value,
output_filename=filename,
output_content_type=content_type,
output_bytes=file_bytes,
completed_by_user_id=int(user.id),
)
except ValueError:
db.rollback()
return RedirectResponse(url=f"/services/work-tracker/tasks/{task.id}/edit?tool_error=result_rejected", status_code=303)
return RedirectResponse(url=f"/services/work-tracker/tasks/{task.id}/edit?tool_saved=1", status_code=303)
finally:
db.close()
@router.get("/tool-runs/{run_id}/output")
def task_tool_result_download(request: Request, run_id: int):
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url="/login", status_code=303)
tenant_id = _active_tenant_id(request, user)
run = get_task_tool_run(db, tenant_id=tenant_id, run_id=run_id)
if not run or not run.output_bytes:
return RedirectResponse(url="/services/work-tracker", status_code=303)
branch_id = _active_branch_id(request, user, db)
task = get_task(
db,
tenant_id=tenant_id,
task_id=int(run.task_instance_id),
branch_id=branch_id,
assigned_to_user_id=_assigned_user_filter(db, user),
partner_user_id=_partner_visibility_user_id(db, user),
financial_year=_active_financial_year(request),
)
if not task:
return _redirect_denied()
filename = (run.output_filename or f"task-tool-run-{run.id}.bin").replace('"', "")
return Response(
content=bytes(run.output_bytes),
media_type=run.output_content_type or "application/octet-stream",
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
)
finally:
db.close()