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.
110 lines
2.8 KiB
Go
110 lines
2.8 KiB
Go
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)
|
|
}
|
|
}
|