package services import ( "context" "encoding/base64" "encoding/json" "fmt" "time" "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/option" "github.com/google/uuid" "github.com/jmoiron/sqlx" "mgit.msbls.de/m/KanzlAI-mGMT/internal/models" ) type AIService struct { client anthropic.Client db *sqlx.DB } func NewAIService(apiKey string, db *sqlx.DB) *AIService { client := anthropic.NewClient(option.WithAPIKey(apiKey)) return &AIService{client: client, db: db} } // ExtractedDeadline represents a deadline extracted by AI from a document. type ExtractedDeadline struct { Title string `json:"title"` DueDate *string `json:"due_date"` DurationValue int `json:"duration_value"` DurationUnit string `json:"duration_unit"` Timing string `json:"timing"` TriggerEvent string `json:"trigger_event"` RuleReference string `json:"rule_reference"` Confidence float64 `json:"confidence"` SourceQuote string `json:"source_quote"` } type extractDeadlinesToolInput struct { Deadlines []ExtractedDeadline `json:"deadlines"` } var deadlineExtractionTool = anthropic.ToolParam{ Name: "extract_deadlines", Description: anthropic.String("Extract all legal deadlines found in the document. Return each deadline with its details."), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "deadlines": map[string]any{ "type": "array", "description": "List of extracted deadlines", "items": map[string]any{ "type": "object", "properties": map[string]any{ "title": map[string]any{ "type": "string", "description": "Short title describing the deadline (e.g. 'Statement of Defence', 'Reply to Counterclaim')", }, "due_date": map[string]any{ "type": []string{"string", "null"}, "description": "Absolute due date in YYYY-MM-DD format if determinable, null otherwise", }, "duration_value": map[string]any{ "type": "integer", "description": "Numeric duration value (e.g. 3 for '3 months')", }, "duration_unit": map[string]any{ "type": "string", "enum": []string{"days", "weeks", "months"}, "description": "Unit of the duration period", }, "timing": map[string]any{ "type": "string", "enum": []string{"after", "before"}, "description": "Whether the deadline is before or after the trigger event", }, "trigger_event": map[string]any{ "type": "string", "description": "The event that triggers this deadline (e.g. 'service of the Statement of Claim')", }, "rule_reference": map[string]any{ "type": "string", "description": "Legal rule reference (e.g. 'Rule 23 RoP', 'Rule 222 RoP', '§ 276 ZPO')", }, "confidence": map[string]any{ "type": "number", "minimum": 0, "maximum": 1, "description": "Confidence score from 0.0 to 1.0", }, "source_quote": map[string]any{ "type": "string", "description": "The exact quote from the document where this deadline was found", }, }, "required": []string{"title", "duration_value", "duration_unit", "timing", "trigger_event", "rule_reference", "confidence", "source_quote"}, }, }, }, Required: []string{"deadlines"}, }, } const extractionSystemPrompt = `You are a legal deadline extraction assistant for German and UPC (Unified Patent Court) patent litigation. Your task is to extract all legal deadlines, time limits, and procedural time periods from the provided document. For each deadline found, extract: - A clear title describing the deadline - The absolute due date if it can be determined from the document - The duration (value + unit: days/weeks/months) - Whether it runs before or after a trigger event - The trigger event that starts the deadline - The legal rule reference (e.g. Rule 23 RoP, § 276 ZPO) - Your confidence level (0.0-1.0) in the extraction - The exact source quote from the document Be thorough: extract every deadline mentioned, including conditional ones. If a deadline references another deadline (e.g. "within 2 months of the defence"), capture that relationship in the trigger_event field. If the document contains no deadlines, return an empty list.` // ExtractDeadlines sends a document (PDF or text) to Claude for deadline extraction. func (s *AIService) ExtractDeadlines(ctx context.Context, pdfData []byte, text string) ([]ExtractedDeadline, error) { var contentBlocks []anthropic.ContentBlockParamUnion if len(pdfData) > 0 { encoded := base64.StdEncoding.EncodeToString(pdfData) contentBlocks = append(contentBlocks, anthropic.ContentBlockParamUnion{ OfDocument: &anthropic.DocumentBlockParam{ Source: anthropic.DocumentBlockParamSourceUnion{ OfBase64: &anthropic.Base64PDFSourceParam{ Data: encoded, }, }, }, }) contentBlocks = append(contentBlocks, anthropic.NewTextBlock("Extract all legal deadlines from this document.")) } else if text != "" { contentBlocks = append(contentBlocks, anthropic.NewTextBlock("Extract all legal deadlines from the following text:\n\n"+text)) } else { return nil, fmt.Errorf("either pdf_data or text must be provided") } msg, err := s.client.Messages.New(ctx, anthropic.MessageNewParams{ Model: anthropic.ModelClaudeSonnet4_5, MaxTokens: 4096, System: []anthropic.TextBlockParam{ {Text: extractionSystemPrompt}, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(contentBlocks...), }, Tools: []anthropic.ToolUnionParam{ {OfTool: &deadlineExtractionTool}, }, ToolChoice: anthropic.ToolChoiceParamOfTool("extract_deadlines"), }) if err != nil { return nil, fmt.Errorf("claude API call: %w", err) } // Find the tool_use block in the response for _, block := range msg.Content { if block.Type == "tool_use" && block.Name == "extract_deadlines" { var input extractDeadlinesToolInput if err := json.Unmarshal(block.Input, &input); err != nil { return nil, fmt.Errorf("parsing tool output: %w", err) } return input.Deadlines, nil } } return nil, fmt.Errorf("no tool_use block in response") } const summarizeSystemPrompt = `You are a legal case summary assistant for German and UPC patent litigation case management. Given a case's details, recent events, and deadlines, produce a concise 2-3 sentence summary of what matters right now. Focus on: - The most urgent upcoming deadline - Recent significant events - The current procedural stage Write in clear, professional language suitable for a lawyer reviewing their case list. Be specific about dates and deadlines.` // SummarizeCase generates an AI summary for a case and caches it in the database. func (s *AIService) SummarizeCase(ctx context.Context, tenantID, caseID uuid.UUID) (string, error) { // Load case var c models.Case err := s.db.GetContext(ctx, &c, "SELECT * FROM cases WHERE id = $1 AND tenant_id = $2", caseID, tenantID) if err != nil { return "", fmt.Errorf("loading case: %w", err) } // Load recent events var events []models.CaseEvent if err := s.db.SelectContext(ctx, &events, "SELECT * FROM case_events WHERE case_id = $1 AND tenant_id = $2 ORDER BY created_at DESC LIMIT 10", caseID, tenantID); err != nil { return "", fmt.Errorf("loading events: %w", err) } // Load active deadlines var deadlines []models.Deadline if err := s.db.SelectContext(ctx, &deadlines, "SELECT * FROM deadlines WHERE case_id = $1 AND tenant_id = $2 AND status = 'active' ORDER BY due_date ASC LIMIT 10", caseID, tenantID); err != nil { return "", fmt.Errorf("loading deadlines: %w", err) } // Build context text caseInfo := fmt.Sprintf("Case: %s — %s\nStatus: %s", c.CaseNumber, c.Title, c.Status) if c.Court != nil { caseInfo += fmt.Sprintf("\nCourt: %s", *c.Court) } if c.CourtRef != nil { caseInfo += fmt.Sprintf("\nCourt Reference: %s", *c.CourtRef) } if c.CaseType != nil { caseInfo += fmt.Sprintf("\nType: %s", *c.CaseType) } eventText := "\n\nRecent Events:" if len(events) == 0 { eventText += "\nNo events recorded." } for _, e := range events { eventText += fmt.Sprintf("\n- [%s] %s", e.CreatedAt.Format("2006-01-02"), e.Title) if e.Description != nil { eventText += fmt.Sprintf(": %s", *e.Description) } } deadlineText := "\n\nUpcoming Deadlines:" if len(deadlines) == 0 { deadlineText += "\nNo active deadlines." } for _, d := range deadlines { deadlineText += fmt.Sprintf("\n- %s: due %s (status: %s)", d.Title, d.DueDate, d.Status) if d.Description != nil { deadlineText += fmt.Sprintf(" — %s", *d.Description) } } prompt := caseInfo + eventText + deadlineText msg, err := s.client.Messages.New(ctx, anthropic.MessageNewParams{ Model: anthropic.ModelClaudeSonnet4_5, MaxTokens: 512, System: []anthropic.TextBlockParam{ {Text: summarizeSystemPrompt}, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Summarize the current state of this case:\n\n" + prompt)), }, }) if err != nil { return "", fmt.Errorf("claude API call: %w", err) } // Extract text from response var summary string for _, block := range msg.Content { if block.Type == "text" { summary += block.Text } } if summary == "" { return "", fmt.Errorf("empty response from Claude") } // Cache summary in database _, err = s.db.ExecContext(ctx, "UPDATE cases SET ai_summary = $1, updated_at = $2 WHERE id = $3 AND tenant_id = $4", summary, time.Now(), caseID, tenantID) if err != nil { return "", fmt.Errorf("caching summary: %w", err) } return summary, nil }