Refine AgentFeed UI and local workflow

This commit is contained in:
peter 2026-06-11 14:01:16 +01:00
parent 3179f832fc
commit ba9f5eb168
29 changed files with 2986 additions and 69 deletions

View file

@ -1,4 +1,5 @@
import html
import json
import mimetypes
import secrets
from pathlib import Path
@ -39,6 +40,10 @@ class ArtifactListCreateView(APIView):
artifacts = artifacts.filter(day=day)
if tag := request.query_params.get("tag"):
artifacts = artifacts.filter(tags__contains=[tag])
if kind := request.query_params.get("kind"):
artifacts = artifacts.filter(kind=kind)
if title_query := request.query_params.get("q"):
artifacts = artifacts.filter(title__icontains=title_query)
return Response([serialize_artifact(artifact, request) for artifact in artifacts])
@ -50,7 +55,7 @@ class ArtifactListCreateView(APIView):
artifact = create_binary_artifact(request.data)
if isinstance(artifact, Response):
return artifact
return Response({"id": artifact.id, "url": reverse("artifact", args=[artifact.id])}, status=201)
return Response(serialize_artifact(artifact, request), status=201)
data = request.data
kind = data.get("kind")
@ -62,16 +67,81 @@ class ArtifactListCreateView(APIView):
if missing:
return Response({"detail": f"Missing required field(s): {', '.join(missing)}"}, status=400)
source_error = validate_source_fields(data)
if source_error:
return source_error
artifact = create_text_artifact(data, request)
return Response({"id": artifact.id, "url": reverse("artifact", args=[artifact.id])}, status=201)
return Response(serialize_artifact(artifact, request), status=201)
class ArtifactDetailView(APIView):
def get(self, request, artifact_id):
artifact = get_object_or_404(Artifact, pk=artifact_id)
return Response(serialize_artifact(artifact, request))
def patch(self, request, artifact_id):
if not has_valid_token(request):
return HttpResponseForbidden("Invalid or missing API token")
artifact = get_object_or_404(Artifact, pk=artifact_id)
data = request.data
if "title" in data:
if not data["title"]:
return Response({"detail": "title cannot be blank"}, status=400)
artifact.title = data["title"]
if "tags" in data:
artifact.tags = normalize_tags(data["tags"])
if "source_prompt" in data:
artifact.source_prompt = data["source_prompt"] or ""
if "source_type" in data:
if data["source_type"] not in {choice[0] for choice in Artifact.SOURCE_TYPE_CHOICES}:
return Response({"detail": "source_type must be agent, person, or system"}, status=400)
artifact.source_type = data["source_type"]
if "source_name" in data:
artifact.source_name = data["source_name"] or ""
if "source_id" in data:
artifact.source_id = data["source_id"] or ""
if "metadata" in data:
if not isinstance(data["metadata"], dict):
return Response({"detail": "metadata must be an object"}, status=400)
artifact.metadata = data["metadata"]
artifact.save(
update_fields=[
"title",
"tags",
"source_prompt",
"source_type",
"source_name",
"source_id",
"metadata",
]
)
return Response(serialize_artifact(artifact, request))
class CapabilitiesView(APIView):
def get(self, request):
return Response(
{
"artifact_kinds": [choice[0] for choice in Artifact.KIND_CHOICES],
"filters": ["persona", "day", "tag", "kind", "q"],
"views": ["rendered", "raw"],
"raw_supported_for": [Artifact.MARKDOWN, Artifact.HTML_FULL],
"source_types": [choice[0] for choice in Artifact.SOURCE_TYPE_CHOICES],
"auth": "bearer_token",
}
)
def feed(request):
persona = request.GET.get("persona", "")
selected_kind = request.GET.get("kind", "")
title_query = request.GET.get("q", "")
selected_day = parse_date(request.GET.get("day", "")) or timezone.localdate()
today = timezone.localdate()
selected_day = parse_date(request.GET.get("day", "")) or today
selected_artifact_id = request.GET.get("artifact", "")
preview_none = request.GET.get("preview") == "none"
selected_view = "raw" if request.GET.get("view") == "raw" else "rendered"
all_artifacts = Artifact.objects.all()
artifacts = all_artifacts
@ -83,12 +153,13 @@ def feed(request):
artifacts = artifacts.filter(title__icontains=title_query)
days = list(artifacts.dates("day", "day", order="DESC"))
if selected_day not in days:
days.append(selected_day)
days.sort(reverse=True)
for day in (today, selected_day):
if day not in days:
days.append(day)
days.sort(reverse=True)
day_artifacts = artifacts.filter(day=selected_day).order_by("-created_at")
selected_artifact = day_artifacts.filter(pk=selected_artifact_id).first()
selected_artifact = None if preview_none else day_artifacts.filter(pk=selected_artifact_id).first()
toggle_query = {"day": selected_day.isoformat(), "view": "rendered"}
if persona:
toggle_query["persona"] = persona
@ -116,7 +187,8 @@ def feed(request):
"selected_kind": selected_kind,
"selected_persona": persona,
"selected_view": selected_view,
"today": timezone.localdate(),
"preview_none": preview_none,
"today": today,
"title_query": title_query,
"kind_choices": Artifact.KIND_CHOICES,
},
@ -177,6 +249,7 @@ def create_text_artifact(data, request):
original_kind=kind,
tags=normalize_tags(data.get("tags", [])),
source_prompt=data.get("source_prompt", ""),
**source_fields(data, persona),
)
@ -188,6 +261,9 @@ def create_binary_artifact(data):
missing.append("file")
if missing:
return Response({"detail": f"Missing required field(s): {', '.join(missing)}"}, status=400)
source_error = validate_source_fields(data)
if source_error:
return source_error
now = timezone.localtime()
artifact_id = generate_id()
@ -217,6 +293,7 @@ def create_binary_artifact(data):
original_kind=kind,
tags=normalize_tags(data.get("tags", [])),
source_prompt=data.get("source_prompt", ""),
**source_fields(data, persona),
)
@ -275,6 +352,41 @@ def normalize_tags(tags):
return []
def validate_source_fields(data):
if source_type := data.get("source_type"):
if source_type not in {choice[0] for choice in Artifact.SOURCE_TYPE_CHOICES}:
return Response({"detail": "source_type must be agent, person, or system"}, status=400)
try:
parse_metadata_value(data.get("metadata", {}))
except ValueError as exc:
return Response({"detail": str(exc)}, status=400)
return None
def source_fields(data, persona):
return {
"source_type": data.get("source_type") or Artifact.AGENT,
"source_name": data.get("source_name") or persona,
"source_id": data.get("source_id") or "",
"metadata": parse_metadata_value(data.get("metadata", {})),
}
def parse_metadata_value(value):
if value in (None, ""):
return {}
if isinstance(value, dict):
return value
if isinstance(value, str):
try:
parsed = json.loads(value)
except json.JSONDecodeError as exc:
raise ValueError("metadata must be an object") from exc
if isinstance(parsed, dict):
return parsed
raise ValueError("metadata must be an object")
def binary_kind(content_type):
if content_type.startswith("image/"):
return Artifact.IMAGE
@ -294,6 +406,9 @@ def serialize_artifact(artifact, request):
return {
"id": artifact.id,
"url": request.build_absolute_uri(reverse("artifact", args=[artifact.id])),
"api_url": request.build_absolute_uri(reverse("artifact-detail-api", args=[artifact.id])),
"raw_url": request.build_absolute_uri(f"{reverse('artifact', args=[artifact.id])}?raw=1"),
"raw_available": raw_artifact_path(artifact) is not None,
"persona": artifact.persona,
"title": artifact.title,
"kind": artifact.kind,
@ -302,6 +417,10 @@ def serialize_artifact(artifact, request):
"created_at": artifact.created_at.isoformat(),
"tags": artifact.tags,
"source_prompt": artifact.source_prompt,
"source_type": artifact.source_type,
"source_name": artifact.source_name,
"source_id": artifact.source_id,
"metadata": artifact.metadata,
}