feat: add AI deadline extraction and case summary (Phase 2J)
This commit is contained in:
115
backend/internal/handlers/ai.go
Normal file
115
backend/internal/handlers/ai.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
"mgit.msbls.de/m/KanzlAI-mGMT/internal/services"
|
||||
)
|
||||
|
||||
type AIHandler struct {
|
||||
ai *services.AIService
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
func NewAIHandler(ai *services.AIService, db *sqlx.DB) *AIHandler {
|
||||
return &AIHandler{ai: ai, db: db}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
deadlines, err := h.ai.ExtractDeadlines(r.Context(), pdfData, text)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "AI extraction failed: "+err.Error())
|
||||
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, err := resolveTenant(r, h.db)
|
||||
if err != nil {
|
||||
handleTenantError(w, err)
|
||||
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 {
|
||||
writeError(w, http.StatusInternalServerError, "AI summarization failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]string{
|
||||
"case_id": caseID.String(),
|
||||
"summary": summary,
|
||||
})
|
||||
}
|
||||
@@ -83,3 +83,8 @@ func handleTenantError(w http.ResponseWriter, err error) {
|
||||
func parsePathUUID(r *http.Request, key string) (uuid.UUID, error) {
|
||||
return uuid.Parse(r.PathValue(key))
|
||||
}
|
||||
|
||||
// parseUUID parses a UUID string
|
||||
func parseUUID(s string) (uuid.UUID, error) {
|
||||
return uuid.Parse(s)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user