Add two Claude API-powered endpoints: - POST /api/ai/extract-deadlines: accepts PDF upload or JSON text, extracts legal deadlines using Claude tool_use for structured output - POST /api/ai/summarize-case: generates AI summary from case events/deadlines, caches result in cases.ai_summary New files: - internal/services/ai_service.go: AIService with Anthropic SDK integration - internal/handlers/ai.go: HTTP handlers for both endpoints - internal/services/ai_service_test.go: tool schema and serialization tests Uses anthropic-sdk-go v1.27.1 with Claude Sonnet 4.5. AI service is optional — endpoints only registered when ANTHROPIC_API_KEY is set.
116 lines
2.8 KiB
Go
116 lines
2.8 KiB
Go
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,
|
|
})
|
|
}
|