Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public class AgentflowProperties {
private Auth auth = new Auth();
private Temporal temporal = new Temporal();
private Retention retention = new Retention();
private Attachments attachments = new Attachments();

public String getVersion() {
return version;
Expand Down Expand Up @@ -88,6 +89,14 @@ public void setRetention(Retention retention) {
this.retention = retention;
}

public Attachments getAttachments() {
return attachments;
}

public void setAttachments(Attachments attachments) {
this.attachments = attachments;
}

public static class Auth {
private boolean enabled = false;
private List<ApiKeyEntry> keys = List.of();
Expand Down Expand Up @@ -453,4 +462,25 @@ public void setPurgeBatchSize(int purgeBatchSize) {
this.purgeBatchSize = purgeBatchSize;
}
}

public static class Attachments {
private String storageDir = "./data/attachments";
private long maxBytes = 10L * 1024 * 1024;

public String getStorageDir() {
return storageDir;
}

public void setStorageDir(String storageDir) {
this.storageDir = storageDir;
}

public long getMaxBytes() {
return maxBytes;
}

public void setMaxBytes(long maxBytes) {
this.maxBytes = maxBytes;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package io.agentflow.api.controller;

import io.agentflow.api.dto.AttachmentResponse;
import io.agentflow.api.service.AttachmentService;
import io.agentflow.api.service.AttachmentService.LoadedAttachment;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequestMapping("/v1/attachments")
public class AttachmentsController {

private final AttachmentService service;

public AttachmentsController(AttachmentService service) {
this.service = service;
}

@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@ResponseStatus(HttpStatus.CREATED)
public AttachmentResponse upload(
@RequestParam("file") MultipartFile file,
@RequestParam(value = "caption", required = false) String caption) {
return service.upload(file, caption);
}

@GetMapping("/{id}")
public AttachmentResponse meta(@PathVariable String id) {
return service.getMeta(id);
}

@GetMapping("/{id}/content")
public ResponseEntity<byte[]> content(@PathVariable String id) {
LoadedAttachment loaded = service.getContent(id);
return ResponseEntity.ok()
.header(
HttpHeaders.CONTENT_DISPOSITION,
"inline; filename=\"" + loaded.entity().getFilename() + "\"")
.contentType(MediaType.parseMediaType(loaded.entity().getMediaType()))
.body(loaded.data());
Comment on lines +44 to +50
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import io.agentflow.api.service.AgentNameConflictException;
import io.agentflow.api.service.AgentNotFoundException;
import io.agentflow.api.service.AgentVersionNotFoundException;
import io.agentflow.api.service.AttachmentNotFoundException;
import io.agentflow.api.service.AttachmentTooLargeException;
import io.agentflow.api.service.RegressionExecutionException;
import io.agentflow.api.service.RunComparisonValidationException;
import io.agentflow.api.service.RunConflictException;
Expand Down Expand Up @@ -39,6 +41,22 @@ public ResponseEntity<Map<String, String>> handleThreadNotFound(ThreadNotFoundEx
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("detail", ex.getMessage()));
}

@ExceptionHandler(AttachmentNotFoundException.class)
public ResponseEntity<Map<String, String>> handleAttachmentNotFound(
AttachmentNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(Map.of("detail", "Attachment not found: " + ex.getMessage()));
}

@ExceptionHandler(AttachmentTooLargeException.class)
public ResponseEntity<Map<String, String>> handleAttachmentTooLarge(
AttachmentTooLargeException ex) {
return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE)
.body(Map.of(
"detail",
"Attachment too large: " + ex.getSize() + " > " + ex.getLimit() + " bytes"));
}

@ExceptionHandler(RunNotFoundException.class)
public ResponseEntity<Map<String, String>> handleRunNotFound(RunNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("detail", ex.getMessage()));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package io.agentflow.api.dto;

import io.agentflow.api.entity.AttachmentEntity;
import java.time.Instant;

public class AttachmentResponse {

private String id;
private String tenantId;
private String runId;
private String messageId;
private String mediaType;
private String filename;
private long sizeBytes;
private String sha256;
private String caption;
private String url;
private Instant createdAt;
private Instant updatedAt;

public static AttachmentResponse fromEntity(AttachmentEntity entity) {
AttachmentResponse response = new AttachmentResponse();
response.id = entity.getId();
response.tenantId = entity.getTenantId();
response.runId = entity.getRunId();
response.messageId = entity.getMessageId();
response.mediaType = entity.getMediaType();
response.filename = entity.getFilename();
response.sizeBytes = entity.getSizeBytes();
response.sha256 = entity.getSha256();
response.caption = entity.getCaption();
response.url = "/v1/attachments/" + entity.getId();
response.createdAt = entity.getCreatedAt();
response.updatedAt = entity.getUpdatedAt();
return response;
}

public String getId() {
return id;
}

public String getTenantId() {
return tenantId;
}

public String getRunId() {
return runId;
}

public String getMessageId() {
return messageId;
}

public String getMediaType() {
return mediaType;
}

public String getFilename() {
return filename;
}

public long getSizeBytes() {
return sizeBytes;
}

public String getSha256() {
return sha256;
}

public String getCaption() {
return caption;
}

public String getUrl() {
return url;
}

public Instant getCreatedAt() {
return createdAt;
}

public Instant getUpdatedAt() {
return updatedAt;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package io.agentflow.api.entity;

import com.github.f4b6a3.ulid.UlidCreator;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import jakarta.persistence.Table;
import java.time.Instant;

@Entity
@Table(name = "attachments")
public class AttachmentEntity {

@Id
@Column(length = 26)
private String id;

@Column(name = "tenant_id", nullable = false, length = 64)
private String tenantId = "default";

@Column(name = "run_id", length = 26)
private String runId;

@Column(name = "message_id", length = 26)
private String messageId;

@Column(name = "media_type", nullable = false, length = 128)
private String mediaType;

@Column(nullable = false, length = 512)
private String filename;

@Column(name = "storage_key", nullable = false, length = 512)
private String storageKey;

@Column(name = "size_bytes", nullable = false)
private long sizeBytes;

@Column(nullable = false, length = 64)
private String sha256;

@Column(columnDefinition = "TEXT")
private String caption;

@Column(name = "created_at", nullable = false)
private Instant createdAt;

@Column(name = "updated_at", nullable = false)
private Instant updatedAt;

@PrePersist
void onCreate() {
if (id == null) {
id = UlidCreator.getUlid().toString();
}
Instant now = Instant.now();
if (createdAt == null) {
createdAt = now;
}
updatedAt = now;
}

@PreUpdate
void onUpdate() {
updatedAt = Instant.now();
}

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getTenantId() {
return tenantId;
}

public void setTenantId(String tenantId) {
this.tenantId = tenantId;
}

public String getRunId() {
return runId;
}

public void setRunId(String runId) {
this.runId = runId;
}

public String getMessageId() {
return messageId;
}

public void setMessageId(String messageId) {
this.messageId = messageId;
}

public String getMediaType() {
return mediaType;
}

public void setMediaType(String mediaType) {
this.mediaType = mediaType;
}

public String getFilename() {
return filename;
}

public void setFilename(String filename) {
this.filename = filename;
}

public String getStorageKey() {
return storageKey;
}

public void setStorageKey(String storageKey) {
this.storageKey = storageKey;
}

public long getSizeBytes() {
return sizeBytes;
}

public void setSizeBytes(long sizeBytes) {
this.sizeBytes = sizeBytes;
}

public String getSha256() {
return sha256;
}

public void setSha256(String sha256) {
this.sha256 = sha256;
}

public String getCaption() {
return caption;
}

public void setCaption(String caption) {
this.caption = caption;
}

public Instant getCreatedAt() {
return createdAt;
}

public Instant getUpdatedAt() {
return updatedAt;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.agentflow.api.repository;

import io.agentflow.api.entity.AttachmentEntity;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface AttachmentRepository extends JpaRepository<AttachmentEntity, String> {

Optional<AttachmentEntity> findByIdAndTenantId(String id, String tenantId);

List<AttachmentEntity> findByRunIdAndTenantIdOrderByCreatedAtAsc(String runId, String tenantId);

List<AttachmentEntity> findByRunId(String runId);

List<AttachmentEntity> findByTenantId(String tenantId);

long deleteByRunId(String runId);

long deleteByTenantId(String tenantId);
}
Loading
Loading