feat: add AI deadline extraction and case summary endpoints (Phase 2J)
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.
This commit is contained in:
109
backend/internal/services/ai_service_test.go
Normal file
109
backend/internal/services/ai_service_test.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDeadlineExtractionToolSchema(t *testing.T) {
|
||||
// Verify the tool schema serializes correctly
|
||||
data, err := json.Marshal(deadlineExtractionTool)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal tool: %v", err)
|
||||
}
|
||||
|
||||
var parsed map[string]any
|
||||
if err := json.Unmarshal(data, &parsed); err != nil {
|
||||
t.Fatalf("failed to unmarshal tool JSON: %v", err)
|
||||
}
|
||||
|
||||
if parsed["name"] != "extract_deadlines" {
|
||||
t.Errorf("expected name 'extract_deadlines', got %v", parsed["name"])
|
||||
}
|
||||
|
||||
schema, ok := parsed["input_schema"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("input_schema is not a map")
|
||||
}
|
||||
|
||||
if schema["type"] != "object" {
|
||||
t.Errorf("expected schema type 'object', got %v", schema["type"])
|
||||
}
|
||||
|
||||
props, ok := schema["properties"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("properties is not a map")
|
||||
}
|
||||
|
||||
deadlines, ok := props["deadlines"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("deadlines property is not a map")
|
||||
}
|
||||
|
||||
if deadlines["type"] != "array" {
|
||||
t.Errorf("expected deadlines type 'array', got %v", deadlines["type"])
|
||||
}
|
||||
|
||||
items, ok := deadlines["items"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("items is not a map")
|
||||
}
|
||||
|
||||
itemProps, ok := items["properties"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("item properties is not a map")
|
||||
}
|
||||
|
||||
expectedFields := []string{"title", "due_date", "duration_value", "duration_unit", "timing", "trigger_event", "rule_reference", "confidence", "source_quote"}
|
||||
for _, field := range expectedFields {
|
||||
if _, ok := itemProps[field]; !ok {
|
||||
t.Errorf("missing expected field %q in item properties", field)
|
||||
}
|
||||
}
|
||||
|
||||
required, ok := items["required"].([]any)
|
||||
if !ok {
|
||||
t.Fatal("required is not a list")
|
||||
}
|
||||
if len(required) != 8 {
|
||||
t.Errorf("expected 8 required fields, got %d", len(required))
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractedDeadlineJSON(t *testing.T) {
|
||||
dueDate := "2026-04-15"
|
||||
d := ExtractedDeadline{
|
||||
Title: "Statement of Defence",
|
||||
DueDate: &dueDate,
|
||||
DurationValue: 3,
|
||||
DurationUnit: "months",
|
||||
Timing: "after",
|
||||
TriggerEvent: "service of the Statement of Claim",
|
||||
RuleReference: "Rule 23 RoP",
|
||||
Confidence: 0.95,
|
||||
SourceQuote: "The defendant shall file a defence within 3 months",
|
||||
}
|
||||
|
||||
data, err := json.Marshal(d)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal: %v", err)
|
||||
}
|
||||
|
||||
var parsed ExtractedDeadline
|
||||
if err := json.Unmarshal(data, &parsed); err != nil {
|
||||
t.Fatalf("failed to unmarshal: %v", err)
|
||||
}
|
||||
|
||||
if parsed.Title != d.Title {
|
||||
t.Errorf("title mismatch: %q != %q", parsed.Title, d.Title)
|
||||
}
|
||||
if *parsed.DueDate != *d.DueDate {
|
||||
t.Errorf("due_date mismatch: %q != %q", *parsed.DueDate, *d.DueDate)
|
||||
}
|
||||
if parsed.DurationValue != d.DurationValue {
|
||||
t.Errorf("duration_value mismatch: %d != %d", parsed.DurationValue, d.DurationValue)
|
||||
}
|
||||
if parsed.Confidence != d.Confidence {
|
||||
t.Errorf("confidence mismatch: %f != %f", parsed.Confidence, d.Confidence)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user