Refine AgentFeed UI and local workflow
This commit is contained in:
parent
3179f832fc
commit
ba9f5eb168
29 changed files with 2986 additions and 69 deletions
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue