feat: AI-powered features — document drafting, case strategy, similar case finder (P2)
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).
This commit is contained in:
@@ -5,6 +5,8 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"mgit.msbls.de/m/KanzlAI-mGMT/internal/auth"
|
||||
"mgit.msbls.de/m/KanzlAI-mGMT/internal/services"
|
||||
)
|
||||
@@ -115,3 +117,139 @@ func (h *AIHandler) SummarizeCase(w http.ResponseWriter, r *http.Request) {
|
||||
"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),
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user