feat: complete multimodal ticket intake
This commit is contained in:
@@ -1,10 +1,15 @@
|
||||
import json
|
||||
import time
|
||||
|
||||
from celery.result import AsyncResult
|
||||
from django.conf import settings
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.http import StreamingHttpResponse
|
||||
from django.utils import timezone
|
||||
from ninja import Router
|
||||
from ninja.errors import HttpError
|
||||
|
||||
from apps.accounts.api import require_permission
|
||||
from apps.accounts.api import require_login, require_permission
|
||||
from apps.agents.models import (
|
||||
AgentDefinition,
|
||||
AgentRun,
|
||||
@@ -24,20 +29,21 @@ from apps.agents.schemas import (
|
||||
)
|
||||
from apps.agents.services import (
|
||||
AgentValidationError,
|
||||
build_config_snapshot,
|
||||
append_run_event,
|
||||
ensure_agent_draft,
|
||||
queue_agent_run,
|
||||
replace_agent_steps,
|
||||
validate_agent_version,
|
||||
)
|
||||
from apps.agents.tasks import execute_agent_run
|
||||
from apps.audit.models import OperationAuditLog
|
||||
from apps.skills.services import SkillValidationError, validate_payload
|
||||
from apps.skills.services import SkillValidationError
|
||||
|
||||
router = Router(tags=["agents"])
|
||||
runs_router = Router(tags=["agent-runs"])
|
||||
|
||||
|
||||
def serialize_step(step: AgentStepBinding) -> dict:
|
||||
deployment = step.model_deployment or step.agent_version.model_deployment
|
||||
return {
|
||||
"id": step.id,
|
||||
"code": step.code,
|
||||
@@ -46,6 +52,9 @@ def serialize_step(step: AgentStepBinding) -> dict:
|
||||
"skill_name": step.skill_version.definition.name,
|
||||
"skill_version": step.skill_version.version,
|
||||
"skill_status": step.skill_version.status,
|
||||
"model_deployment_id": step.model_deployment_id,
|
||||
"model_display_name": deployment.display_name,
|
||||
"model_capability": deployment.capability,
|
||||
"input_mapping": step.input_mapping,
|
||||
}
|
||||
|
||||
@@ -60,6 +69,7 @@ def serialize_version(version: AgentVersion) -> dict:
|
||||
"system_prompt": version.system_prompt,
|
||||
"input_schema": version.input_schema,
|
||||
"example_input": version.example_input,
|
||||
"allowed_actions": version.allowed_actions,
|
||||
"temperature_override": version.temperature_override,
|
||||
"max_output_tokens_override": version.max_output_tokens_override,
|
||||
"steps": [serialize_step(step) for step in version.steps.all()],
|
||||
@@ -96,6 +106,9 @@ def serialize_run(run: AgentRun, detailed: bool = False) -> dict:
|
||||
"status": run.status,
|
||||
"sandbox": run.sandbox,
|
||||
"current_step": run.current_step,
|
||||
"progress_stage": run.progress_stage,
|
||||
"progress_message": run.progress_message,
|
||||
"retry_count": run.retry_count,
|
||||
"total_steps": run.total_steps,
|
||||
"input_tokens": run.input_tokens,
|
||||
"output_tokens": run.output_tokens,
|
||||
@@ -119,6 +132,17 @@ def serialize_run(run: AgentRun, detailed: bool = False) -> dict:
|
||||
"code": step.code,
|
||||
"name": step.name,
|
||||
"order": step.order,
|
||||
"model_deployment_id": step.model_deployment_id,
|
||||
"model_display_name": (
|
||||
step.model_deployment.display_name
|
||||
if step.model_deployment_id
|
||||
else ""
|
||||
),
|
||||
"model_capability": (
|
||||
step.model_deployment.capability
|
||||
if step.model_deployment_id
|
||||
else ""
|
||||
),
|
||||
"status": step.status,
|
||||
"input_payload": step.input_payload,
|
||||
"output_payload": step.output_payload,
|
||||
@@ -136,6 +160,48 @@ def serialize_run(run: AgentRun, detailed: bool = False) -> dict:
|
||||
return payload
|
||||
|
||||
|
||||
def get_run_for_stream(request, public_id: str) -> AgentRun:
|
||||
user = require_login(request)
|
||||
run = AgentRun.objects.select_related("definition", "agent_version").filter(
|
||||
public_id=public_id
|
||||
).first()
|
||||
if run is None:
|
||||
raise HttpError(404, "Agent 运行不存在")
|
||||
if (
|
||||
run.created_by_id == user.id
|
||||
or user.is_superuser
|
||||
or user.has_perm("system.agent.run.view")
|
||||
):
|
||||
return run
|
||||
raise HttpError(403, "Permission denied")
|
||||
|
||||
|
||||
def parse_event_cursor(request, after: int | None) -> int:
|
||||
header_value = request.headers.get("Last-Event-ID", "").strip()
|
||||
try:
|
||||
header_cursor = int(header_value) if header_value else 0
|
||||
except ValueError as error:
|
||||
raise HttpError(400, "Last-Event-ID 必须是非负整数") from error
|
||||
query_cursor = after or 0
|
||||
if header_cursor < 0 or query_cursor < 0:
|
||||
raise HttpError(400, "事件游标必须是非负整数")
|
||||
return max(header_cursor, query_cursor)
|
||||
|
||||
|
||||
def format_sse_event(event) -> str:
|
||||
data = json.dumps(
|
||||
{
|
||||
"sequence": event.sequence,
|
||||
"event_type": event.event_type,
|
||||
"payload": event.payload,
|
||||
"created_at": event.created_at.isoformat(),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
separators=(",", ":"),
|
||||
)
|
||||
return f"id: {event.sequence}\nevent: {event.event_type}\ndata: {data}\n\n"
|
||||
|
||||
|
||||
def audit(user, action: str, target_type: str, target_id: str, snapshot: dict) -> None:
|
||||
OperationAuditLog.objects.create(
|
||||
user=user,
|
||||
@@ -173,6 +239,7 @@ def create_agent(request, payload: AgentCreateIn):
|
||||
system_prompt=payload.system_prompt,
|
||||
input_schema=payload.input_schema,
|
||||
example_input=payload.example_input,
|
||||
allowed_actions=payload.allowed_actions,
|
||||
temperature_override=payload.temperature_override,
|
||||
max_output_tokens_override=payload.max_output_tokens_override,
|
||||
created_by=request.user,
|
||||
@@ -315,24 +382,14 @@ def start_agent_run(request, agent_id: int, payload: AgentRunIn):
|
||||
)
|
||||
sandbox = False
|
||||
try:
|
||||
validate_agent_version(version, publishing=not sandbox)
|
||||
validate_payload(version.input_schema, payload.input, "Agent 输入")
|
||||
run = queue_agent_run(
|
||||
version,
|
||||
payload.input,
|
||||
created_by=request.user,
|
||||
sandbox=sandbox,
|
||||
)
|
||||
except (AgentValidationError, SkillValidationError) as error:
|
||||
raise HttpError(400, str(error)) from error
|
||||
run = AgentRun.objects.create(
|
||||
definition=definition,
|
||||
agent_version=version,
|
||||
sandbox=sandbox,
|
||||
input_payload=payload.input,
|
||||
config_snapshot=build_config_snapshot(version),
|
||||
total_steps=version.steps.count(),
|
||||
created_by=request.user,
|
||||
)
|
||||
result = execute_agent_run.delay(run.id)
|
||||
run.refresh_from_db()
|
||||
if not run.task_id:
|
||||
run.task_id = result.id or ""
|
||||
run.save(update_fields=["task_id"])
|
||||
audit(
|
||||
request.user,
|
||||
"run_started",
|
||||
@@ -356,11 +413,47 @@ def list_agent_runs(request, status: str | None = None, agent_id: int | None = N
|
||||
|
||||
@runs_router.get("/{public_id}", response=AgentRunDetailOut)
|
||||
def get_agent_run(request, public_id: str):
|
||||
require_permission(request, "agent.run.view")
|
||||
run = AgentRun.objects.select_related("definition", "agent_version").get(public_id=public_id)
|
||||
run = get_run_for_stream(request, public_id)
|
||||
return serialize_run(run, detailed=True)
|
||||
|
||||
|
||||
@runs_router.get("/{public_id}/events")
|
||||
def stream_agent_run_events(request, public_id: str, after: int | None = None):
|
||||
run = get_run_for_stream(request, public_id)
|
||||
cursor = parse_event_cursor(request, after)
|
||||
terminal_events = {"completed", "failed", "cancelled", "timed_out", "needs_input"}
|
||||
max_seconds = getattr(settings, "AGENT_RUN_SSE_MAX_SECONDS", 60)
|
||||
|
||||
def event_stream():
|
||||
current = cursor
|
||||
started = time.monotonic()
|
||||
last_heartbeat = started
|
||||
while time.monotonic() - started < max_seconds:
|
||||
events = list(run.events.filter(sequence__gt=current).order_by("sequence")[:100])
|
||||
for event in events:
|
||||
current = event.sequence
|
||||
yield format_sse_event(event)
|
||||
if events and events[-1].event_type in terminal_events:
|
||||
return
|
||||
latest = run.events.order_by("-sequence").first()
|
||||
if (
|
||||
latest is not None
|
||||
and latest.sequence <= current
|
||||
and latest.event_type in terminal_events
|
||||
):
|
||||
return
|
||||
now = time.monotonic()
|
||||
if now - last_heartbeat >= 15:
|
||||
yield ": keep-alive\n\n"
|
||||
last_heartbeat = now
|
||||
time.sleep(0.5)
|
||||
|
||||
response = StreamingHttpResponse(event_stream(), content_type="text/event-stream")
|
||||
response["Cache-Control"] = "no-cache"
|
||||
response["X-Accel-Buffering"] = "no"
|
||||
return response
|
||||
|
||||
|
||||
@runs_router.post("/{public_id}/cancel", response=AgentRunDetailOut)
|
||||
def cancel_agent_run(request, public_id: str):
|
||||
require_permission(request, "agent.use")
|
||||
@@ -371,9 +464,13 @@ def cancel_agent_run(request, public_id: str):
|
||||
update_fields = ["cancellation_requested_at"]
|
||||
if run.status == "queued":
|
||||
run.status = "cancelled"
|
||||
run.progress_stage = "cancelled"
|
||||
run.progress_message = "运行已取消"
|
||||
run.finished_at = timezone.now()
|
||||
update_fields.extend(["status", "finished_at"])
|
||||
update_fields.extend(["status", "progress_stage", "progress_message", "finished_at"])
|
||||
run.save(update_fields=update_fields)
|
||||
if run.status == "cancelled":
|
||||
append_run_event(run, "cancelled", {"message": "运行已取消"})
|
||||
if run.task_id:
|
||||
AsyncResult(run.task_id).revoke(terminate=False)
|
||||
audit(request.user, "cancel", "agent_run", str(run.public_id), {"status": run.status})
|
||||
|
||||
Reference in New Issue
Block a user