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

@@ -16,10 +16,13 @@ package openai
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/openai/openai-go"
@@ -188,6 +191,8 @@ func buildMessages(messages []llm.Message) []openai.ChatCompletionMessageParamUn
},
),
)
case llm.FilePart:
parts = append(parts, buildFilePart(p))
}
}
out = append(out, openai.UserMessage(parts))
@@ -450,3 +455,23 @@ func mapChunkToEvent(chunk *openai.ChatCompletionChunk) llm.ChatCompletionStream
return event
}
func buildFilePart(p llm.FilePart) openai.ChatCompletionContentPartUnionParam {
switch {
case strings.HasPrefix(p.MimeType, "image/"):
return openai.ImageContentPart(openai.ChatCompletionContentPartImageImageURLParam{
URL: fmt.Sprintf("data:%s;base64,%s", p.MimeType, p.Data),
})
case strings.HasPrefix(p.MimeType, "text/"):
decoded, err := base64.StdEncoding.DecodeString(p.Data)
if err != nil {
return openai.TextContentPart(fmt.Sprintf("[file: %s, type: %s, error decoding content]", p.Filename, p.MimeType))
}
return openai.TextContentPart(fmt.Sprintf("File: %s\n\n%s", p.Filename, string(decoded)))
default:
return openai.FileContentPart(openai.ChatCompletionContentPartFileFileParam{
FileData: param.NewOpt(fmt.Sprintf("data:%s;base64,%s", p.MimeType, p.Data)),
Filename: param.NewOpt(p.Filename),
})
}
}