Add AI-powered evidence description generation

Introduce a background worker that automatically generates
compliance-focused descriptions for uploaded evidence files
using configurable LLM providers. Descriptions are surfaced
across all interfaces: GraphQL API, MCP API, CLI, and the
console UI.

Key changes:
- Multi-provider LLM config with per-agent settings (pointer
  types for Temperature/MaxTokens to preserve zero values)
- Evidence description worker with bounded concurrency
- EvidenceDescriptionStatus typed enum with PostgreSQL enum type
- New `prb evidence` CLI commands (list, view, delete)
- Evidence description displayed in console table and preview
- Migration only marks evidences without files as completed

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2026-03-26 14:30:04 +01:00
parent 8bbed534a9
commit 619ec7b882
24 changed files with 1390 additions and 125 deletions

View File

@@ -125,39 +125,48 @@ function EvidencePreviewContent({
);
}
let preview;
if (evidence.file.mimeType?.startsWith("image/")) {
return (
preview = (
<img
src={evidence.file.downloadUrl}
alt={evidence.file.fileName}
className="max-h-[70vh] object-contain"
/>
);
}
if (evidence.file.mimeType?.includes("pdf")) {
return (
} else if (evidence.file.mimeType?.includes("pdf")) {
preview = (
<iframe
src={evidence.file.downloadUrl}
className="w-full h-[70vh]"
title={evidence.file.fileName}
/>
);
} else {
preview = (
<div className="flex flex-col items-center gap-2 justify-center">
<IconWarning size={20} />
<p className="text-txt-secondary text-center">
{__("Preview not available for this file type")
+ " "
+ evidence.file.mimeType}
</p>
<Button asChild variant="secondary" icon={IconArrowInbox}>
<a href={evidence.file.downloadUrl} target="_blank" rel="noreferrer">
{__("Download File")}
</a>
</Button>
</div>
);
}
return (
<div className="flex flex-col items-center gap-2 justify-center">
<IconWarning size={20} />
<p className="text-txt-secondary text-center">
{__("Preview not available for this file type")
+ " "
+ evidence.file.mimeType}
</p>
<Button asChild variant="secondary" icon={IconArrowInbox}>
<a href={evidence.file.downloadUrl} target="_blank" rel="noreferrer">
{__("Download File")}
</a>
</Button>
<div className="space-y-4">
{preview}
{evidence.description && (
<p className="text-txt-secondary text-sm">{evidence.description}</p>
)}
</div>
);
}

View File

@@ -1,4 +1,4 @@
import { fileSize, fileType, formatDate, sprintf } from "@probo/helpers";
import { fileSize, formatDate, sprintf } from "@probo/helpers";
import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import {
@@ -79,7 +79,7 @@ export const evidenceFragment = graphql`
mimeType
size
}
type
description
createdAt
canDelete: permission(action: "core:evidence:delete")
}
@@ -123,8 +123,8 @@ export default function MeasureEvidencesTab() {
<SortableTable {...pagination}>
<Thead>
<Tr>
<Th>{__("Evidence name")}</Th>
<Th>{__("Type")}</Th>
<Th>{__("Description")}</Th>
<Th>{__("File type")}</Th>
<Th>{__("File size")}</Th>
<Th>{__("Created at")}</Th>
<Th width={50}></Th>
@@ -248,13 +248,12 @@ function EvidenceRow(props: {
/>
)}
<Tr to={evidenceUrl}>
<Td>{evidence.file?.fileName}</Td>
<Td>
{fileType(__, {
type: evidence.type,
mimeType: evidence.file?.mimeType || "",
})}
<span className="text-txt-secondary text-sm line-clamp-2">
{evidence.description || "—"}
</span>
</Td>
<Td>{evidence.file?.mimeType || "—"}</Td>
<Td>{fileSize(__, evidence.file?.size || 0)}</Td>
<Td>{formatDate(evidence.createdAt)}</Td>
<Td noLink>