Backend: - DraftDocument: Claude generates legal documents from case data + template type (14 template types: Klageschrift, UPC claims, Abmahnung, etc.) - CaseStrategy: Opus-powered strategic analysis with next steps, risk assessment, and timeline optimization (structured tool output) - FindSimilarCases: queries youpc.org Supabase for UPC cases, Claude ranks by relevance with explanations and key holdings Endpoints: POST /api/ai/draft-document, /case-strategy, /similar-cases All rate-limited (5 req/min) and permission-gated (PermAIExtraction). YouPC database connection is optional (YOUPC_DATABASE_URL env var).
256 lines
6.4 KiB
Go
256 lines
6.4 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"mgit.msbls.de/m/KanzlAI-mGMT/internal/auth"
|
|
"mgit.msbls.de/m/KanzlAI-mGMT/internal/services"
|
|
)
|
|
|
|
type AIHandler struct {
|
|
ai *services.AIService
|
|
}
|
|
|
|
func NewAIHandler(ai *services.AIService) *AIHandler {
|
|
return &AIHandler{ai: ai}
|
|
}
|
|
|
|
// ExtractDeadlines handles POST /api/ai/extract-deadlines
|
|
// Accepts either multipart/form-data with a "file" PDF field, or JSON {"text": "..."}.
|
|
func (h *AIHandler) ExtractDeadlines(w http.ResponseWriter, r *http.Request) {
|
|
contentType := r.Header.Get("Content-Type")
|
|
|
|
var pdfData []byte
|
|
var text string
|
|
|
|
// Check if multipart (PDF upload)
|
|
if len(contentType) >= 9 && contentType[:9] == "multipart" {
|
|
if err := r.ParseMultipartForm(32 << 20); err != nil { // 32MB max
|
|
writeError(w, http.StatusBadRequest, "failed to parse multipart form")
|
|
return
|
|
}
|
|
file, _, err := r.FormFile("file")
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "missing 'file' field in multipart form")
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
pdfData, err = io.ReadAll(file)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "failed to read uploaded file")
|
|
return
|
|
}
|
|
} else {
|
|
// Assume JSON body
|
|
var body struct {
|
|
Text string `json:"text"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
text = body.Text
|
|
}
|
|
|
|
if len(pdfData) == 0 && text == "" {
|
|
writeError(w, http.StatusBadRequest, "provide either a PDF file or text")
|
|
return
|
|
}
|
|
if len(text) > maxDescriptionLen {
|
|
writeError(w, http.StatusBadRequest, "text exceeds maximum length")
|
|
return
|
|
}
|
|
|
|
deadlines, err := h.ai.ExtractDeadlines(r.Context(), pdfData, text)
|
|
if err != nil {
|
|
internalError(w, "AI deadline extraction failed", err)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"deadlines": deadlines,
|
|
"count": len(deadlines),
|
|
})
|
|
}
|
|
|
|
// SummarizeCase handles POST /api/ai/summarize-case
|
|
// Accepts JSON {"case_id": "uuid"}.
|
|
func (h *AIHandler) SummarizeCase(w http.ResponseWriter, r *http.Request) {
|
|
tenantID, ok := auth.TenantFromContext(r.Context())
|
|
if !ok {
|
|
writeError(w, http.StatusForbidden, "missing tenant")
|
|
return
|
|
}
|
|
|
|
var body struct {
|
|
CaseID string `json:"case_id"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
if body.CaseID == "" {
|
|
writeError(w, http.StatusBadRequest, "case_id is required")
|
|
return
|
|
}
|
|
|
|
caseID, err := parseUUID(body.CaseID)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid case_id")
|
|
return
|
|
}
|
|
|
|
summary, err := h.ai.SummarizeCase(r.Context(), tenantID, caseID)
|
|
if err != nil {
|
|
internalError(w, "AI case summarization failed", err)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]string{
|
|
"case_id": caseID.String(),
|
|
"summary": summary,
|
|
})
|
|
}
|
|
|
|
// DraftDocument handles POST /api/ai/draft-document
|
|
// Accepts JSON {"case_id": "uuid", "template_type": "string", "instructions": "string", "language": "de|en|fr"}.
|
|
func (h *AIHandler) DraftDocument(w http.ResponseWriter, r *http.Request) {
|
|
tenantID, ok := auth.TenantFromContext(r.Context())
|
|
if !ok {
|
|
writeError(w, http.StatusForbidden, "missing tenant")
|
|
return
|
|
}
|
|
|
|
var body struct {
|
|
CaseID string `json:"case_id"`
|
|
TemplateType string `json:"template_type"`
|
|
Instructions string `json:"instructions"`
|
|
Language string `json:"language"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
if body.CaseID == "" {
|
|
writeError(w, http.StatusBadRequest, "case_id is required")
|
|
return
|
|
}
|
|
if body.TemplateType == "" {
|
|
writeError(w, http.StatusBadRequest, "template_type is required")
|
|
return
|
|
}
|
|
|
|
caseID, err := parseUUID(body.CaseID)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid case_id")
|
|
return
|
|
}
|
|
|
|
if len(body.Instructions) > maxDescriptionLen {
|
|
writeError(w, http.StatusBadRequest, "instructions exceeds maximum length")
|
|
return
|
|
}
|
|
|
|
draft, err := h.ai.DraftDocument(r.Context(), tenantID, caseID, body.TemplateType, body.Instructions, body.Language)
|
|
if err != nil {
|
|
internalError(w, "AI document drafting failed", err)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, draft)
|
|
}
|
|
|
|
// CaseStrategy handles POST /api/ai/case-strategy
|
|
// Accepts JSON {"case_id": "uuid"}.
|
|
func (h *AIHandler) CaseStrategy(w http.ResponseWriter, r *http.Request) {
|
|
tenantID, ok := auth.TenantFromContext(r.Context())
|
|
if !ok {
|
|
writeError(w, http.StatusForbidden, "missing tenant")
|
|
return
|
|
}
|
|
|
|
var body struct {
|
|
CaseID string `json:"case_id"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
if body.CaseID == "" {
|
|
writeError(w, http.StatusBadRequest, "case_id is required")
|
|
return
|
|
}
|
|
|
|
caseID, err := parseUUID(body.CaseID)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid case_id")
|
|
return
|
|
}
|
|
|
|
strategy, err := h.ai.CaseStrategy(r.Context(), tenantID, caseID)
|
|
if err != nil {
|
|
internalError(w, "AI case strategy analysis failed", err)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, strategy)
|
|
}
|
|
|
|
// SimilarCases handles POST /api/ai/similar-cases
|
|
// Accepts JSON {"case_id": "uuid", "description": "string"}.
|
|
func (h *AIHandler) SimilarCases(w http.ResponseWriter, r *http.Request) {
|
|
tenantID, ok := auth.TenantFromContext(r.Context())
|
|
if !ok {
|
|
writeError(w, http.StatusForbidden, "missing tenant")
|
|
return
|
|
}
|
|
|
|
var body struct {
|
|
CaseID string `json:"case_id"`
|
|
Description string `json:"description"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
if body.CaseID == "" && body.Description == "" {
|
|
writeError(w, http.StatusBadRequest, "either case_id or description is required")
|
|
return
|
|
}
|
|
|
|
if len(body.Description) > maxDescriptionLen {
|
|
writeError(w, http.StatusBadRequest, "description exceeds maximum length")
|
|
return
|
|
}
|
|
|
|
var caseID uuid.UUID
|
|
if body.CaseID != "" {
|
|
var err error
|
|
caseID, err = parseUUID(body.CaseID)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid case_id")
|
|
return
|
|
}
|
|
}
|
|
|
|
cases, err := h.ai.FindSimilarCases(r.Context(), tenantID, caseID, body.Description)
|
|
if err != nil {
|
|
internalError(w, "AI similar case search failed", err)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"cases": cases,
|
|
"count": len(cases),
|
|
})
|
|
}
|